From 628497f46fb7c71ba29f841e36906203731e0db6 Mon Sep 17 00:00:00 2001
From: Lucas B <lucas@jito.wtf>
Date: Thu, 25 Aug 2022 17:18:46 -0500
Subject: [PATCH] Jito Patch Use cluster info functions for tpu (#345) (#347)
 Use git rev-parse for git sha Remove blacklisted tx from
 message_hash_to_transaction (Backport #374) (#376) Updates scripts for easy
 local setup. (#383) Backports sim_bundle improvements (#407) backports clone
 derivation 416 (#417) Backport #419: add upsert to AccountOverrides (#420)

backports #430 v1.16: update jito-programs sha (#431)

[JIT-1661] Faster Autosnapshot (#406)

Fix Buildkite warnings (#437)

Backport #446 to v1.16 (#447)

backport 428, runtime plugin (#429)

v1.16: Backport #449 (#450)
---
 .dockerignore                                 |    9 +
 .github/workflows/client-targets.yml          |    4 +
 .github/workflows/crate-check.yml             |    1 +
 .github/workflows/docs.yml                    |    3 +
 .github/workflows/downstream-project-spl.yml  |    6 +
 .../increment-cargo-version-on-release.yml    |    2 +
 .github/workflows/release-artifacts.yml       |    1 +
 .gitignore                                    |    6 +-
 .gitmodules                                   |    9 +
 Cargo.lock                                    |  468 ++++-
 Cargo.toml                                    |   12 +
 README.md                                     |  141 +-
 SECURITY.md                                   |  167 --
 anchor                                        |    1 +
 banking-bench/src/main.rs                     |   14 +-
 banks-server/Cargo.toml                       |    5 +
 banks-server/src/banks_server.rs              |    5 +-
 bootstrap                                     |   28 +
 bundle/Cargo.toml                             |   34 +
 bundle/src/bundle_execution.rs                | 1186 ++++++++++++
 bundle/src/lib.rs                             |   60 +
 ci/buildkite-pipeline-in-disk.sh              |    4 +-
 ci/buildkite-pipeline.sh                      |    4 +-
 ci/channel-info.sh                            |    2 +-
 ci/check-crates.sh                            |    3 +
 core/Cargo.toml                               |   12 +
 core/benches/banking_stage.rs                 |   26 +-
 core/benches/cluster_info.rs                  |    1 +
 core/benches/consumer.rs                      |   24 +-
 core/benches/proto_to_packet.rs               |   56 +
 core/benches/retransmit_stage.rs              |    1 +
 core/src/accounts_hash_verifier.rs            |    7 +-
 core/src/admin_rpc_post_init.rs               |    7 +-
 core/src/banking_stage.rs                     |   66 +-
 core/src/banking_stage/committer.rs           |   17 +-
 core/src/banking_stage/consume_worker.rs      |   11 +-
 core/src/banking_stage/consumer.rs            |  158 +-
 core/src/banking_trace.rs                     |    1 +
 core/src/broadcast_stage.rs                   |   49 +-
 .../broadcast_duplicates_run.rs               |    1 +
 .../broadcast_fake_shreds_run.rs              |    1 +
 core/src/broadcast_stage/broadcast_utils.rs   |   60 +-
 .../fail_entry_verification_broadcast_run.rs  |    4 +-
 .../broadcast_stage/standard_broadcast_run.rs |   30 +-
 core/src/bundle_stage.rs                      |  435 +++++
 .../src/bundle_stage/bundle_account_locker.rs |  328 ++++
 core/src/bundle_stage/bundle_consumer.rs      | 1589 +++++++++++++++++
 .../bundle_packet_deserializer.rs             |  286 +++
 .../bundle_stage/bundle_packet_receiver.rs    |  848 +++++++++
 .../bundle_reserved_space_manager.rs          |  189 ++
 .../bundle_stage_leader_metrics.rs            |  502 ++++++
 core/src/bundle_stage/committer.rs            |  221 +++
 core/src/bundle_stage/result.rs               |   41 +
 core/src/consensus_cache_updater.rs           |   52 +
 core/src/immutable_deserialized_bundle.rs     |  483 +++++
 core/src/latest_unprocessed_votes.rs          |    2 +-
 core/src/lib.rs                               |   44 +
 core/src/packet_bundle.rs                     |    7 +
 core/src/proxy/auth.rs                        |  185 ++
 core/src/proxy/block_engine_stage.rs          |  533 ++++++
 core/src/proxy/fetch_stage_manager.rs         |  170 ++
 core/src/proxy/mod.rs                         |  100 ++
 core/src/proxy/relayer_stage.rs               |  495 +++++
 core/src/qos_service.rs                       |   45 +-
 core/src/replay_stage.rs                      |    4 +-
 core/src/retransmit_stage.rs                  |   16 +-
 core/src/snapshot_packager_service.rs         |   19 +-
 core/src/tip_manager.rs                       |  583 ++++++
 core/src/tpu.rs                               |  113 +-
 core/src/tpu_entry_notifier.rs                |   60 +-
 core/src/tvu.rs                               |    5 +-
 core/src/unprocessed_transaction_storage.rs   |  782 ++++----
 core/src/validator.rs                         |   53 +-
 core/tests/epoch_accounts_hash.rs             |    2 +
 core/tests/snapshots.rs                       |    5 +
 deploy_programs                               |   17 +
 dev/Dockerfile                                |   48 +
 entry/src/entry.rs                            |    2 +-
 entry/src/poh.rs                              |   29 +-
 f                                             |   30 +
 fetch-spl.sh                                  |   41 +-
 frozen-abi/macro/src/lib.rs                   |    2 +-
 .../src/geyser_plugin_manager.rs              |   36 +-
 gossip/src/cluster_info.rs                    |    4 +
 jito-programs                                 |    1 +
 jito-protos/Cargo.toml                        |   19 +
 jito-protos/build.rs                          |   38 +
 jito-protos/protos                            |    1 +
 jito-protos/src/lib.rs                        |   25 +
 ledger-tool/src/ledger_utils.rs               |   16 +-
 ledger-tool/src/main.rs                       |   31 +-
 ledger-tool/src/program.rs                    |    1 +
 ledger/src/bank_forks_utils.rs                |   21 +-
 ledger/src/blockstore_processor.rs            |    5 +-
 ledger/src/token_balances.rs                  |   58 +-
 local-cluster/src/local_cluster.rs            |    3 +
 .../src/local_cluster_snapshot_utils.rs       |    6 +-
 local-cluster/src/validator_configs.rs        |    5 +
 local-cluster/tests/local_cluster.rs          |   12 +-
 merkle-tree/src/merkle_tree.rs                |   46 +-
 multinode-demo/bootstrap-validator.sh         |   34 +
 multinode-demo/validator.sh                   |   40 +
 perf/src/sigverify.rs                         |    2 +-
 poh/src/poh_recorder.rs                       |  138 +-
 poh/src/poh_service.rs                        |   34 +-
 program-runtime/src/timings.rs                |   23 +-
 program-test/src/programs.rs                  |   17 +
 .../programs/jito_tip_distribution-0.1.3.so   |  Bin 0 -> 439968 bytes
 .../src/programs/jito_tip_payment-0.1.3.so    |  Bin 0 -> 433224 bytes
 programs/sbf/Cargo.lock                       |  321 +++-
 programs/sbf/tests/programs.rs                |    4 +-
 rpc-client-api/Cargo.toml                     |    2 +
 rpc-client-api/src/bundles.rs                 |  166 ++
 rpc-client-api/src/config.rs                  |    2 +-
 rpc-client-api/src/lib.rs                     |    1 +
 rpc-client-api/src/request.rs                 |    3 +
 rpc-client-api/src/response.rs                |   16 +
 rpc-client/src/http_sender.rs                 |  209 ++-
 rpc-client/src/mock_sender.rs                 |    7 +
 rpc-client/src/nonblocking/rpc_client.rs      |  131 +-
 rpc-client/src/rpc_client.rs                  |   30 +
 rpc-client/src/rpc_sender.rs                  |    4 +
 rpc-test/Cargo.toml                           |    1 +
 rpc-test/tests/rpc.rs                         |    2 +
 rpc/Cargo.toml                                |    2 +
 rpc/src/rpc.rs                                |  488 ++++-
 rpc/src/rpc_service.rs                        |    9 +-
 runtime-plugin/Cargo.toml                     |   23 +
 runtime-plugin/src/lib.rs                     |    4 +
 runtime-plugin/src/runtime_plugin.rs          |   41 +
 .../src/runtime_plugin_admin_rpc_service.rs   |  326 ++++
 runtime-plugin/src/runtime_plugin_manager.rs  |  275 +++
 runtime-plugin/src/runtime_plugin_service.rs  |  123 ++
 runtime/src/account_overrides.rs              |    6 +-
 runtime/src/accounts.rs                       |   98 +-
 runtime/src/bank.rs                           |  110 +-
 runtime/src/cost_tracker.rs                   |    8 +
 runtime/src/snapshot_package.rs               |    8 +-
 runtime/src/snapshot_utils.rs                 |   30 +-
 runtime/src/stake_account.rs                  |    4 +-
 runtime/src/stakes.rs                         |   12 +-
 runtime/src/transaction_batch.rs              |   24 +-
 rustfmt.toml                                  |    5 +
 s                                             |   15 +
 scripts/increment-cargo-version.sh            |    2 +
 scripts/run.sh                                |    4 +
 sdk/Cargo.toml                                |    1 +
 sdk/src/bundle/mod.rs                         |   33 +
 sdk/src/lib.rs                                |    1 +
 send-transaction-service/Cargo.toml           |    2 +
 .../src/send_transaction_service.rs           |   47 +-
 start                                         |    9 +
 start_multi                                   |   30 +
 test-validator/src/lib.rs                     |    1 +
 tip-distributor/Cargo.toml                    |   54 +
 tip-distributor/README.md                     |   52 +
 tip-distributor/src/bin/claim-mev-tips.rs     |  171 ++
 .../src/bin/merkle-root-generator.rs          |   34 +
 .../src/bin/merkle-root-uploader.rs           |   54 +
 .../src/bin/stake-meta-generator.rs           |   67 +
 tip-distributor/src/claim_mev_workflow.rs     |  448 +++++
 tip-distributor/src/lib.rs                    | 1083 +++++++++++
 .../src/merkle_root_generator_workflow.rs     |   54 +
 .../src/merkle_root_upload_workflow.rs        |  138 ++
 tip-distributor/src/reclaim_rent_workflow.rs  |  247 +++
 .../src/stake_meta_generator_workflow.rs      |  952 ++++++++++
 transaction-status/src/lib.rs                 |    9 +-
 validator/Cargo.toml                          |    2 +
 validator/src/admin_rpc_service.rs            |  116 +-
 validator/src/bootstrap.rs                    |    9 +-
 validator/src/cli.rs                          |  237 +++
 validator/src/dashboard.rs                    |    1 +
 validator/src/main.rs                         |  268 ++-
 version/src/lib.rs                            |    2 +-
 174 files changed, 17093 insertions(+), 1234 deletions(-)
 create mode 100644 .dockerignore
 create mode 100644 .gitmodules
 delete mode 100644 SECURITY.md
 create mode 160000 anchor
 create mode 100755 bootstrap
 create mode 100644 bundle/Cargo.toml
 create mode 100644 bundle/src/bundle_execution.rs
 create mode 100644 bundle/src/lib.rs
 create mode 100644 core/benches/proto_to_packet.rs
 create mode 100644 core/src/bundle_stage.rs
 create mode 100644 core/src/bundle_stage/bundle_account_locker.rs
 create mode 100644 core/src/bundle_stage/bundle_consumer.rs
 create mode 100644 core/src/bundle_stage/bundle_packet_deserializer.rs
 create mode 100644 core/src/bundle_stage/bundle_packet_receiver.rs
 create mode 100644 core/src/bundle_stage/bundle_reserved_space_manager.rs
 create mode 100644 core/src/bundle_stage/bundle_stage_leader_metrics.rs
 create mode 100644 core/src/bundle_stage/committer.rs
 create mode 100644 core/src/bundle_stage/result.rs
 create mode 100644 core/src/consensus_cache_updater.rs
 create mode 100644 core/src/immutable_deserialized_bundle.rs
 create mode 100644 core/src/packet_bundle.rs
 create mode 100644 core/src/proxy/auth.rs
 create mode 100644 core/src/proxy/block_engine_stage.rs
 create mode 100644 core/src/proxy/fetch_stage_manager.rs
 create mode 100644 core/src/proxy/mod.rs
 create mode 100644 core/src/proxy/relayer_stage.rs
 create mode 100644 core/src/tip_manager.rs
 create mode 100755 deploy_programs
 create mode 100644 dev/Dockerfile
 create mode 100755 f
 create mode 160000 jito-programs
 create mode 100644 jito-protos/Cargo.toml
 create mode 100644 jito-protos/build.rs
 create mode 160000 jito-protos/protos
 create mode 100644 jito-protos/src/lib.rs
 create mode 100644 program-test/src/programs/jito_tip_distribution-0.1.3.so
 create mode 100644 program-test/src/programs/jito_tip_payment-0.1.3.so
 create mode 100644 rpc-client-api/src/bundles.rs
 create mode 100644 runtime-plugin/Cargo.toml
 create mode 100644 runtime-plugin/src/lib.rs
 create mode 100644 runtime-plugin/src/runtime_plugin.rs
 create mode 100644 runtime-plugin/src/runtime_plugin_admin_rpc_service.rs
 create mode 100644 runtime-plugin/src/runtime_plugin_manager.rs
 create mode 100644 runtime-plugin/src/runtime_plugin_service.rs
 create mode 100755 s
 create mode 100644 sdk/src/bundle/mod.rs
 create mode 100755 start
 create mode 100755 start_multi
 create mode 100644 tip-distributor/Cargo.toml
 create mode 100644 tip-distributor/README.md
 create mode 100644 tip-distributor/src/bin/claim-mev-tips.rs
 create mode 100644 tip-distributor/src/bin/merkle-root-generator.rs
 create mode 100644 tip-distributor/src/bin/merkle-root-uploader.rs
 create mode 100644 tip-distributor/src/bin/stake-meta-generator.rs
 create mode 100644 tip-distributor/src/claim_mev_workflow.rs
 create mode 100644 tip-distributor/src/lib.rs
 create mode 100644 tip-distributor/src/merkle_root_generator_workflow.rs
 create mode 100644 tip-distributor/src/merkle_root_upload_workflow.rs
 create mode 100644 tip-distributor/src/reclaim_rent_workflow.rs
 create mode 100644 tip-distributor/src/stake_meta_generator_workflow.rs

diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000..99262ca894
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+.dockerignore
+.git/
+.github/
+.gitignore
+.idea/
+README.md
+Dockerfile
+f
+target/
diff --git a/.github/workflows/client-targets.yml b/.github/workflows/client-targets.yml
index 3b3d1779a1..fd1971e894 100644
--- a/.github/workflows/client-targets.yml
+++ b/.github/workflows/client-targets.yml
@@ -30,6 +30,8 @@ jobs:
     runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v3
+        with:
+          submodules: 'recursive'
 
       - run: cargo install cargo-ndk@2.12.2
 
@@ -54,6 +56,8 @@ jobs:
     runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v3
+        with:
+          submodules: 'recursive'
 
       - name: Setup Rust
         run: |
diff --git a/.github/workflows/crate-check.yml b/.github/workflows/crate-check.yml
index a47e7cde5f..9b57d633ad 100644
--- a/.github/workflows/crate-check.yml
+++ b/.github/workflows/crate-check.yml
@@ -18,6 +18,7 @@ jobs:
       - uses: actions/checkout@v3
         with:
           fetch-depth: 0
+          submodules: 'recursive'
 
       - name: Get commit range (push)
         if: ${{ github.event_name == 'push' }}
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index fb2096bd33..e5ac907ea1 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -22,6 +22,7 @@ jobs:
         uses: actions/checkout@v3
         with:
           fetch-depth: 0
+          submodules: 'recursive'
 
       - name: Get commit range (push)
         if: ${{ github.event_name == 'push' }}
@@ -77,6 +78,8 @@ jobs:
     steps:
       - name: Checkout
         uses: actions/checkout@v3
+        with:
+          submodules: 'recursive'
 
       - name: Setup Node
         uses: actions/setup-node@v3
diff --git a/.github/workflows/downstream-project-spl.yml b/.github/workflows/downstream-project-spl.yml
index 9934defc6d..f5070beb2d 100644
--- a/.github/workflows/downstream-project-spl.yml
+++ b/.github/workflows/downstream-project-spl.yml
@@ -36,6 +36,8 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
+        with:
+          submodules: 'recursive'
 
       - shell: bash
         run: |
@@ -86,6 +88,8 @@ jobs:
           ]
     steps:
       - uses: actions/checkout@v3
+        with:
+          submodules: 'recursive'
 
       - shell: bash
         run: |
@@ -139,6 +143,8 @@ jobs:
 
     steps:
       - uses: actions/checkout@v3
+        with:
+          submodules: 'recursive'
 
       - shell: bash
         run: |
diff --git a/.github/workflows/increment-cargo-version-on-release.yml b/.github/workflows/increment-cargo-version-on-release.yml
index 5592d76ca5..ca55af2155 100644
--- a/.github/workflows/increment-cargo-version-on-release.yml
+++ b/.github/workflows/increment-cargo-version-on-release.yml
@@ -11,6 +11,8 @@ jobs:
     steps:
       - name: Checkout Repository
         uses: actions/checkout@v3
+        with:
+          submodules: 'recursive'
 
       # This script confirms two assumptions:
       # 1) Tag should be branch.<patch_version>
diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml
index 3e5ab89fe3..7090907550 100644
--- a/.github/workflows/release-artifacts.yml
+++ b/.github/workflows/release-artifacts.yml
@@ -26,6 +26,7 @@ jobs:
         with:
           ref: master
           fetch-depth: 0
+          submodules: 'recursive'
 
       - name: Setup Rust
         shell: bash
diff --git a/.gitignore b/.gitignore
index 3167a9d720..f891833b15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@
 /solana-release.tar.bz2
 /solana-metrics/
 /solana-metrics.tar.bz2
-/target/
+**/target/
 /test-ledger/
 
 **/*.rs.bk
@@ -27,7 +27,11 @@ log-*/
 # fetch-spl.sh artifacts
 /spl-genesis-args.sh
 /spl_*.so
+/jito_*.so
 
 .DS_Store
 # scripts that may be generated by cargo *-bpf commands
 **/cargo-*-bpf-child-script-*.sh
+
+.env
+docker-output/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..e31fc7fccd
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,9 @@
+[submodule "anchor"]
+	path = anchor
+	url = https://github.com/jito-foundation/anchor.git
+[submodule "jito-programs"]
+	path = jito-programs
+	url = https://github.com/jito-foundation/jito-programs.git
+[submodule "jito-protos/protos"]
+	path = jito-protos/protos
+	url = https://github.com/jito-labs/mev-protos.git
diff --git a/Cargo.lock b/Cargo.lock
index 5b8f1d0989..c6abd21a11 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -116,6 +116,145 @@ dependencies = [
  "alloc-no-stdlib",
 ]
 
+[[package]]
+name = "anchor-attribute-access-control"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "regex",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-account"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "bs58 0.4.0",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "rustversion",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-constant"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "proc-macro2 1.0.60",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-error"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-event"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-interface"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "heck 0.3.3",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-program"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-state"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-derive-accounts"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-lang"
+version = "0.24.2"
+dependencies = [
+ "anchor-attribute-access-control",
+ "anchor-attribute-account",
+ "anchor-attribute-constant",
+ "anchor-attribute-error",
+ "anchor-attribute-event",
+ "anchor-attribute-interface",
+ "anchor-attribute-program",
+ "anchor-attribute-state",
+ "anchor-derive-accounts",
+ "arrayref",
+ "base64 0.13.1",
+ "bincode",
+ "borsh 0.10.3",
+ "bytemuck",
+ "solana-program",
+ "thiserror",
+]
+
+[[package]]
+name = "anchor-syn"
+version = "0.24.2"
+dependencies = [
+ "anyhow",
+ "bs58 0.3.1",
+ "heck 0.3.3",
+ "proc-macro2 1.0.60",
+ "proc-macro2-diagnostics",
+ "quote 1.0.28",
+ "serde",
+ "serde_json",
+ "sha2 0.9.9",
+ "syn 1.0.109",
+ "thiserror",
+]
+
 [[package]]
 name = "android_system_properties"
 version = "0.1.4"
@@ -277,9 +416,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
 
 [[package]]
 name = "arrayvec"
-version = "0.7.2"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
 
 [[package]]
 name = "ascii"
@@ -446,7 +585,7 @@ checksum = "e5694b64066a2459918d8074c2ce0d5a88f409431994c2356617c8ae0c4721fc"
 dependencies = [
  "async-trait",
  "axum-core",
- "bitflags",
+ "bitflags 1.3.2",
  "bytes",
  "futures-util",
  "http",
@@ -537,7 +676,7 @@ version = "0.65.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "cexpr",
  "clang-sys",
  "lazy_static",
@@ -573,6 +712,12 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "bitflags"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
+
 [[package]]
 name = "bitmaps"
 version = "2.1.0"
@@ -584,9 +729,9 @@ dependencies = [
 
 [[package]]
 name = "blake3"
-version = "1.3.3"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef"
+checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87"
 dependencies = [
  "arrayref",
  "arrayvec",
@@ -659,7 +804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b"
 dependencies = [
  "borsh-derive 0.10.3",
- "hashbrown 0.11.2",
+ "hashbrown 0.13.2",
 ]
 
 [[package]]
@@ -753,6 +898,12 @@ dependencies = [
  "alloc-stdlib",
 ]
 
+[[package]]
+name = "bs58"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb"
+
 [[package]]
 name = "bs58"
 version = "0.4.0"
@@ -1011,7 +1162,7 @@ checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
 dependencies = [
  "ansi_term",
  "atty",
- "bitflags",
+ "bitflags 1.3.2",
  "strsim 0.8.0",
  "textwrap 0.11.0",
  "unicode-width",
@@ -1025,9 +1176,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
 dependencies = [
  "atty",
- "bitflags",
- "clap_derive",
- "clap_lex",
+ "bitflags 1.3.2",
+ "clap_derive 3.2.18",
+ "clap_lex 0.2.4",
  "indexmap",
  "once_cell",
  "strsim 0.10.0",
@@ -1035,6 +1186,21 @@ dependencies = [
  "textwrap 0.16.0",
 ]
 
+[[package]]
+name = "clap"
+version = "4.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098"
+dependencies = [
+ "bitflags 2.4.0",
+ "clap_derive 4.1.9",
+ "clap_lex 0.3.3",
+ "is-terminal",
+ "once_cell",
+ "strsim 0.10.0",
+ "termcolor",
+]
+
 [[package]]
 name = "clap_derive"
 version = "3.2.18"
@@ -1048,6 +1214,19 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "clap_derive"
+version = "4.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644"
+dependencies = [
+ "heck 0.4.0",
+ "proc-macro-error",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "clap_lex"
 version = "0.2.4"
@@ -1057,6 +1236,15 @@ dependencies = [
  "os_str_bytes",
 ]
 
+[[package]]
+name = "clap_lex"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646"
+dependencies = [
+ "os_str_bytes",
+]
+
 [[package]]
 name = "combine"
 version = "3.8.1"
@@ -1140,9 +1328,9 @@ dependencies = [
 
 [[package]]
 name = "constant_time_eq"
-version = "0.2.5"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b"
+checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
 
 [[package]]
 name = "convert_case"
@@ -1394,6 +1582,17 @@ version = "2.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
 
+[[package]]
+name = "default-env"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f753eb82d29277e79efc625e84aecacfd4851ee50e05a8573a4740239a77bfd3"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
+]
+
 [[package]]
 name = "der"
 version = "0.5.1"
@@ -1970,7 +2169,7 @@ version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32c95766e0414f8bfc1d07055574c621b67739466d6ba516c4fef8e99d30d2e6"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "cfg-if 1.0.0",
  "log",
  "managed",
@@ -2160,7 +2359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d"
 dependencies = [
  "base64 0.13.1",
- "bitflags",
+ "bitflags 1.3.2",
  "bytes",
  "headers-core",
  "http",
@@ -2515,6 +2714,18 @@ version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
 
+[[package]]
+name = "is-terminal"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "itertools"
 version = "0.10.5"
@@ -2530,6 +2741,49 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
 
+[[package]]
+name = "jito-programs-vote-state"
+version = "0.1.4"
+dependencies = [
+ "anchor-lang",
+ "bincode",
+ "serde",
+ "serde_derive",
+ "solana-program",
+]
+
+[[package]]
+name = "jito-protos"
+version = "1.16.20"
+dependencies = [
+ "bytes",
+ "prost 0.11.9",
+ "prost-types 0.11.9",
+ "protobuf-src",
+ "tonic 0.8.3",
+ "tonic-build 0.8.4",
+]
+
+[[package]]
+name = "jito-tip-distribution"
+version = "0.1.4"
+dependencies = [
+ "anchor-lang",
+ "default-env",
+ "jito-programs-vote-state",
+ "solana-program",
+ "solana-security-txt",
+]
+
+[[package]]
+name = "jito-tip-payment"
+version = "0.1.4"
+dependencies = [
+ "anchor-lang",
+ "default-env",
+ "solana-security-txt",
+]
+
 [[package]]
 name = "jobserver"
 version = "0.1.24"
@@ -3051,7 +3305,7 @@ version = "0.26.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "cfg-if 1.0.0",
  "libc",
  "memoffset 0.7.1",
@@ -3316,7 +3570,7 @@ version = "0.10.55"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "cfg-if 1.0.0",
  "foreign-types",
  "libc",
@@ -3793,6 +4047,19 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada"
+dependencies = [
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+ "version_check",
+ "yansi",
+]
+
 [[package]]
 name = "proptest"
 version = "1.2.0"
@@ -3800,7 +4067,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65"
 dependencies = [
  "bit-set",
- "bitflags",
+ "bitflags 1.3.2",
  "byteorder",
  "lazy_static",
  "num-traits",
@@ -4196,7 +4463,7 @@ version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
 ]
 
 [[package]]
@@ -4205,7 +4472,7 @@ version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
 ]
 
 [[package]]
@@ -4406,7 +4673,7 @@ version = "0.37.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "errno",
  "io-lifetimes",
  "libc",
@@ -4564,7 +4831,7 @@ version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "core-foundation",
  "core-foundation-sys",
  "libc",
@@ -4937,7 +5204,7 @@ dependencies = [
  "Inflector",
  "base64 0.21.2",
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "bv",
  "lazy_static",
  "serde",
@@ -5086,12 +5353,15 @@ dependencies = [
  "futures 0.3.28",
  "solana-banks-interface",
  "solana-client",
+ "solana-gossip",
  "solana-runtime",
  "solana-sdk",
  "solana-send-transaction-service",
+ "solana-streamer",
  "tarpc",
  "tokio",
  "tokio-serde",
+ "tokio-stream",
 ]
 
 [[package]]
@@ -5210,6 +5480,26 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "solana-bundle"
+version = "1.16.20"
+dependencies = [
+ "anchor-lang",
+ "assert_matches",
+ "itertools",
+ "log",
+ "serde",
+ "solana-ledger",
+ "solana-logger",
+ "solana-measure",
+ "solana-poh",
+ "solana-program-runtime",
+ "solana-runtime",
+ "solana-sdk",
+ "solana-transaction-status",
+ "thiserror",
+]
+
 [[package]]
 name = "solana-cargo-build-bpf"
 version = "1.16.20"
@@ -5299,7 +5589,7 @@ name = "solana-cli"
 version = "1.16.20"
 dependencies = [
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "clap 2.33.3",
  "console",
  "const_format",
@@ -5496,9 +5786,10 @@ dependencies = [
 name = "solana-core"
 version = "1.16.20"
 dependencies = [
+ "anchor-lang",
  "base64 0.21.2",
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "chrono",
  "crossbeam-channel",
  "dashmap 4.0.2",
@@ -5507,12 +5798,17 @@ dependencies = [
  "fs_extra",
  "histogram",
  "itertools",
+ "jito-protos",
+ "jito-tip-distribution",
+ "jito-tip-payment",
  "lazy_static",
  "log",
  "lru",
  "matches",
  "min-max-heap",
  "num_enum 0.6.1",
+ "prost 0.11.9",
+ "prost-types 0.11.9",
  "rand 0.7.3",
  "rand_chacha 0.2.2",
  "raptorq",
@@ -5525,6 +5821,7 @@ dependencies = [
  "serial_test",
  "solana-address-lookup-table-program",
  "solana-bloom",
+ "solana-bundle",
  "solana-client",
  "solana-entry",
  "solana-frozen-abi",
@@ -5539,10 +5836,12 @@ dependencies = [
  "solana-perf",
  "solana-poh",
  "solana-program-runtime",
+ "solana-program-test",
  "solana-rayon-threadlimit",
  "solana-rpc",
  "solana-rpc-client-api",
  "solana-runtime",
+ "solana-runtime-plugin",
  "solana-sdk",
  "solana-send-transaction-service",
  "solana-stake-program",
@@ -5561,6 +5860,8 @@ dependencies = [
  "test-case",
  "thiserror",
  "tokio",
+ "tonic 0.8.3",
+ "tonic-build 0.8.4",
  "trees",
 ]
 
@@ -5669,7 +5970,7 @@ dependencies = [
  "ahash 0.8.3",
  "blake3",
  "block-buffer 0.10.4",
- "bs58",
+ "bs58 0.4.0",
  "bv",
  "byteorder",
  "cc",
@@ -5753,7 +6054,7 @@ dependencies = [
 name = "solana-geyser-plugin-manager"
 version = "1.16.20"
 dependencies = [
- "bs58",
+ "bs58 0.4.0",
  "crossbeam-channel",
  "json5",
  "jsonrpc-core",
@@ -5861,7 +6162,7 @@ dependencies = [
 name = "solana-keygen"
 version = "1.16.20"
 dependencies = [
- "bs58",
+ "bs58 0.4.0",
  "clap 3.2.23",
  "dirs-next",
  "num_cpus",
@@ -5880,8 +6181,8 @@ version = "1.16.20"
 dependencies = [
  "assert_matches",
  "bincode",
- "bitflags",
- "bs58",
+ "bitflags 1.3.2",
+ "bs58 0.4.0",
  "byteorder",
  "chrono",
  "chrono-humanize",
@@ -5943,7 +6244,7 @@ name = "solana-ledger-tool"
 version = "1.16.20"
 dependencies = [
  "assert_cmd",
- "bs58",
+ "bs58 0.4.0",
  "bytecount",
  "chrono",
  "clap 2.33.3",
@@ -6224,11 +6525,11 @@ dependencies = [
  "assert_matches",
  "base64 0.21.2",
  "bincode",
- "bitflags",
+ "bitflags 1.3.2",
  "blake3",
  "borsh 0.10.3",
  "borsh 0.9.3",
- "bs58",
+ "bs58 0.4.0",
  "bv",
  "bytemuck",
  "cc",
@@ -6406,7 +6707,7 @@ version = "1.16.20"
 dependencies = [
  "base64 0.21.2",
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "crossbeam-channel",
  "dashmap 4.0.2",
  "itertools",
@@ -6426,6 +6727,7 @@ dependencies = [
  "soketto",
  "solana-account-decoder",
  "solana-address-lookup-table-program",
+ "solana-bundle",
  "solana-client",
  "solana-entry",
  "solana-faucet",
@@ -6436,6 +6738,7 @@ dependencies = [
  "solana-net-utils",
  "solana-perf",
  "solana-poh",
+ "solana-program-runtime",
  "solana-rayon-threadlimit",
  "solana-rpc-client-api",
  "solana-runtime",
@@ -6466,7 +6769,7 @@ dependencies = [
  "async-trait",
  "base64 0.21.2",
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "crossbeam-channel",
  "futures 0.3.28",
  "indicatif",
@@ -6492,7 +6795,7 @@ name = "solana-rpc-client-api"
 version = "1.16.20"
 dependencies = [
  "base64 0.21.2",
- "bs58",
+ "bs58 0.4.0",
  "jsonrpc-core",
  "reqwest",
  "semver 1.0.17",
@@ -6500,6 +6803,8 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "solana-account-decoder",
+ "solana-bundle",
+ "solana-runtime",
  "solana-sdk",
  "solana-transaction-status",
  "solana-version",
@@ -6529,13 +6834,14 @@ name = "solana-rpc-test"
 version = "1.16.20"
 dependencies = [
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "crossbeam-channel",
  "futures-util",
  "log",
  "reqwest",
  "serde",
  "serde_json",
+ "serial_test",
  "solana-account-decoder",
  "solana-client",
  "solana-logger",
@@ -6625,17 +6931,36 @@ dependencies = [
  "zstd",
 ]
 
+[[package]]
+name = "solana-runtime-plugin"
+version = "1.16.20"
+dependencies = [
+ "crossbeam-channel",
+ "json5",
+ "jsonrpc-core",
+ "jsonrpc-core-client",
+ "jsonrpc-derive",
+ "jsonrpc-ipc-server",
+ "jsonrpc-server-utils",
+ "libloading",
+ "log",
+ "solana-runtime",
+ "solana-sdk",
+ "thiserror",
+]
+
 [[package]]
 name = "solana-sdk"
 version = "1.16.20"
 dependencies = [
+ "anchor-lang",
  "anyhow",
  "assert_matches",
  "base64 0.21.2",
  "bincode",
- "bitflags",
+ "bitflags 1.3.2",
  "borsh 0.10.3",
- "bs58",
+ "bs58 0.4.0",
  "bytemuck",
  "byteorder",
  "chrono",
@@ -6685,13 +7010,19 @@ dependencies = [
 name = "solana-sdk-macro"
 version = "1.16.20"
 dependencies = [
- "bs58",
+ "bs58 0.4.0",
  "proc-macro2 1.0.60",
  "quote 1.0.28",
  "rustversion",
  "syn 2.0.18",
 ]
 
+[[package]]
+name = "solana-security-txt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183"
+
 [[package]]
 name = "solana-send-transaction-service"
 version = "1.16.20"
@@ -6699,11 +7030,13 @@ dependencies = [
  "crossbeam-channel",
  "log",
  "solana-client",
+ "solana-gossip",
  "solana-logger",
  "solana-measure",
  "solana-metrics",
  "solana-runtime",
  "solana-sdk",
+ "solana-streamer",
  "solana-tpu-client",
 ]
 
@@ -6777,7 +7110,7 @@ name = "solana-storage-proto"
 version = "1.16.20"
 dependencies = [
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "enum-iterator",
  "prost 0.11.9",
  "protobuf-src",
@@ -6889,6 +7222,41 @@ dependencies = [
  "solana-sdk",
 ]
 
+[[package]]
+name = "solana-tip-distributor"
+version = "1.16.20"
+dependencies = [
+ "anchor-lang",
+ "clap 4.1.11",
+ "crossbeam-channel",
+ "env_logger",
+ "futures 0.3.28",
+ "gethostname",
+ "im",
+ "itertools",
+ "jito-tip-distribution",
+ "jito-tip-payment",
+ "log",
+ "num-traits",
+ "rand 0.7.3",
+ "serde",
+ "serde_json",
+ "solana-client",
+ "solana-genesis-utils",
+ "solana-ledger",
+ "solana-measure",
+ "solana-merkle-tree",
+ "solana-metrics",
+ "solana-program",
+ "solana-program-runtime",
+ "solana-rpc-client-api",
+ "solana-runtime",
+ "solana-sdk",
+ "solana-stake-program",
+ "thiserror",
+ "tokio",
+]
+
 [[package]]
 name = "solana-tokens"
 version = "1.16.20"
@@ -6980,7 +7348,7 @@ dependencies = [
  "base64 0.21.2",
  "bincode",
  "borsh 0.10.3",
- "bs58",
+ "bs58 0.4.0",
  "lazy_static",
  "log",
  "serde",
@@ -7066,6 +7434,7 @@ dependencies = [
  "solana-rpc-client",
  "solana-rpc-client-api",
  "solana-runtime",
+ "solana-runtime-plugin",
  "solana-sdk",
  "solana-send-transaction-service",
  "solana-storage-bigtable",
@@ -7078,6 +7447,7 @@ dependencies = [
  "symlink",
  "thiserror",
  "tikv-jemallocator",
+ "tonic 0.8.3",
 ]
 
 [[package]]
@@ -7139,7 +7509,7 @@ dependencies = [
 name = "solana-zk-keygen"
 version = "1.16.20"
 dependencies = [
- "bs58",
+ "bs58 0.4.0",
  "clap 3.2.23",
  "dirs-next",
  "num_cpus",
@@ -7581,7 +7951,7 @@ version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "225e483f02d0ad107168dc57381a8a40c3aeea6abe47f37506931f861643cfa8"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "byteorder",
  "libc",
  "thiserror",
@@ -8040,6 +8410,7 @@ dependencies = [
  "pin-project",
  "prost 0.11.9",
  "prost-derive 0.11.9",
+ "rustls-native-certs",
  "rustls-pemfile 1.0.0",
  "tokio",
  "tokio-rustls 0.23.3",
@@ -8050,6 +8421,7 @@ dependencies = [
  "tower-service",
  "tracing",
  "tracing-futures",
+ "webpki-roots",
 ]
 
 [[package]]
@@ -8103,7 +8475,7 @@ version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "bytes",
  "futures-core",
  "futures-util",
@@ -8835,6 +9207,12 @@ dependencies = [
  "linked-hash-map",
 ]
 
+[[package]]
+name = "yansi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+
 [[package]]
 name = "yasna"
 version = "0.5.0"
diff --git a/Cargo.toml b/Cargo.toml
index 8d8bac0243..9233f20f96 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ members = [
     "bench-tps",
     "bloom",
     "bucket_map",
+    "bundle",
     "clap-utils",
     "clap-v3-utils",
     "cli",
@@ -32,6 +33,7 @@ members = [
     "geyser-plugin-manager",
     "gossip",
     "install",
+    "jito-protos",
     "keygen",
     "ledger",
     "ledger-tool",
@@ -76,6 +78,7 @@ members = [
     "rpc-client-nonce-utils",
     "rpc-test",
     "runtime",
+    "runtime-plugin",
     "runtime/store-tool",
     "sdk",
     "sdk/cargo-build-bpf",
@@ -93,6 +96,7 @@ members = [
     "streamer",
     "test-validator",
     "thin-client",
+    "tip-distributor",
     "tokens",
     "tpu-client",
     "transaction-dos",
@@ -107,6 +111,8 @@ members = [
 ]
 
 exclude = [
+    "anchor",
+    "jito-programs",
     "programs/sbf",
 ]
 
@@ -125,6 +131,7 @@ edition = "2021"
 aes-gcm-siv = "0.10.3"
 ahash = "0.8.3"
 anyhow = "1.0.71"
+anchor-lang = { path = "anchor/lang" }
 ark-bn254 = "0.4.0"
 ark-ec = "0.4.0"
 ark-ff = "0.4.0"
@@ -210,6 +217,9 @@ indicatif = "0.17.4"
 Inflector = "0.11.4"
 itertools = "0.10.5"
 jemallocator = { package = "tikv-jemallocator", version = "0.4.1", features = ["unprefixed_malloc_on_supported_platforms"] }
+jito-protos = { path = "jito-protos", version = "=1.16.20" }
+jito-tip-distribution = { path = "jito-programs/mev-programs/programs/tip-distribution", features = ["no-entrypoint"] }
+jito-tip-payment = { path = "jito-programs/mev-programs/programs/tip-payment", features = ["no-entrypoint"] }
 js-sys = "0.3.63"
 json5 = "0.4.1"
 jsonrpc-core = "18.0.0"
@@ -298,6 +308,7 @@ solana-bench-tps = { path = "bench-tps", version = "=1.16.20" }
 solana-bloom = { path = "bloom", version = "=1.16.20" }
 solana-bpf-loader-program = { path = "programs/bpf_loader", version = "=1.16.20" }
 solana-bucket-map = { path = "bucket_map", version = "=1.16.20" }
+solana-bundle = { path = "bundle", version = "=1.16.20" }
 solana-connection-cache = { path = "connection-cache", version = "=1.16.20", default-features = false }
 solana-clap-utils = { path = "clap-utils", version = "=1.16.20" }
 solana-clap-v3-utils = { path = "clap-v3-utils", version = "=1.16.20" }
@@ -341,6 +352,7 @@ solana-rpc-client = { path = "rpc-client", version = "=1.16.20", default-feature
 solana-rpc-client-api = { path = "rpc-client-api", version = "=1.16.20" }
 solana-rpc-client-nonce-utils = { path = "rpc-client-nonce-utils", version = "=1.16.20" }
 solana-runtime = { path = "runtime", version = "=1.16.20" }
+solana-runtime-plugin = { path = "runtime-plugin", version = "=1.16.20" }
 solana-sdk = { path = "sdk", version = "=1.16.20" }
 solana-sdk-macro = { path = "sdk/macro", version = "=1.16.20" }
 solana-send-transaction-service = { path = "send-transaction-service", version = "=1.16.20" }
diff --git a/README.md b/README.md
index 4fccacf2ba..750e797895 100644
--- a/README.md
+++ b/README.md
@@ -4,142 +4,9 @@
   </a>
 </p>
 
-[![Solana crate](https://img.shields.io/crates/v/solana-core.svg)](https://crates.io/crates/solana-core)
-[![Solana documentation](https://docs.rs/solana-core/badge.svg)](https://docs.rs/solana-core)
-[![Build status](https://badge.buildkite.com/8cc350de251d61483db98bdfc895b9ea0ac8ffa4a32ee850ed.svg?branch=master)](https://buildkite.com/solana-labs/solana/builds?branch=master)
-[![codecov](https://codecov.io/gh/solana-labs/solana/branch/master/graph/badge.svg)](https://codecov.io/gh/solana-labs/solana)
+[![Build status](https://badge.buildkite.com/3a7c88c0f777e1a0fddacc190823565271ae4c251ef78d83a8.svg)](https://buildkite.com/jito/jito-solana)
 
-# Building
+# About
+This repository contains Jito's fork of the Solana validator.
 
-## **1. Install rustc, cargo and rustfmt.**
-
-```bash
-$ curl https://sh.rustup.rs -sSf | sh
-$ source $HOME/.cargo/env
-$ rustup component add rustfmt
-```
-
-When building the master branch, please make sure you are using the latest stable rust version by running:
-
-```bash
-$ rustup update
-```
-
-When building a specific release branch, you should check the rust version in `ci/rust-version.sh` and if necessary, install that version by running:
-```bash
-$ rustup install VERSION
-```
-Note that if this is not the latest rust version on your machine, cargo commands may require an [override](https://rust-lang.github.io/rustup/overrides.html) in order to use the correct version.
-
-On Linux systems you may need to install libssl-dev, pkg-config, zlib1g-dev, protobuf etc.
-
-On Ubuntu:
-```bash
-$ sudo apt-get update
-$ sudo apt-get install libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang cmake make libprotobuf-dev protobuf-compiler
-```
-
-On Fedora:
-```bash
-$ sudo dnf install openssl-devel systemd-devel pkg-config zlib-devel llvm clang cmake make protobuf-devel protobuf-compiler perl-core
-```
-
-## **2. Download the source code.**
-
-```bash
-$ git clone https://github.com/solana-labs/solana.git
-$ cd solana
-```
-
-## **3. Build.**
-
-```bash
-$ ./cargo build
-```
-
-# Testing
-
-**Run the test suite:**
-
-```bash
-$ ./cargo test
-```
-
-### Starting a local testnet
-Start your own testnet locally, instructions are in the [online docs](https://docs.solana.com/cluster/bench-tps).
-
-### Accessing the remote development cluster
-* `devnet` - stable public cluster for development accessible via
-devnet.solana.com. Runs 24/7. Learn more about the [public clusters](https://docs.solana.com/clusters)
-
-# Benchmarking
-
-First, install the nightly build of rustc. `cargo bench` requires the use of the
-unstable features only available in the nightly build.
-
-```bash
-$ rustup install nightly
-```
-
-Run the benchmarks:
-
-```bash
-$ cargo +nightly bench
-```
-
-# Release Process
-
-The release process for this project is described [here](RELEASE.md).
-
-# Code coverage
-
-To generate code coverage statistics:
-
-```bash
-$ scripts/coverage.sh
-$ open target/cov/lcov-local/index.html
-```
-
-Why coverage? While most see coverage as a code quality metric, we see it primarily as a developer
-productivity metric. When a developer makes a change to the codebase, presumably it's a *solution* to
-some problem.  Our unit-test suite is how we encode the set of *problems* the codebase solves. Running
-the test suite should indicate that your change didn't *infringe* on anyone else's solutions. Adding a
-test *protects* your solution from future changes. Say you don't understand why a line of code exists,
-try deleting it and running the unit-tests. The nearest test failure should tell you what problem
-was solved by that code. If no test fails, go ahead and submit a Pull Request that asks, "what
-problem is solved by this code?" On the other hand, if a test does fail and you can think of a
-better way to solve the same problem, a Pull Request with your solution would most certainly be
-welcome! Likewise, if rewriting a test can better communicate what code it's protecting, please
-send us that patch!
-
-# Disclaimer
-
-All claims, content, designs, algorithms, estimates, roadmaps,
-specifications, and performance measurements described in this project
-are done with the Solana Labs, Inc. (“SL”) good faith efforts. It is up to
-the reader to check and validate their accuracy and truthfulness.
-Furthermore, nothing in this project constitutes a solicitation for
-investment.
-
-Any content produced by SL or developer resources that SL provides are
-for educational and inspirational purposes only. SL does not encourage,
-induce or sanction the deployment, integration or use of any such
-applications (including the code comprising the Solana blockchain
-protocol) in violation of applicable laws or regulations and hereby
-prohibits any such deployment, integration or use. This includes the use of
-any such applications by the reader (a) in violation of export control
-or sanctions laws of the United States or any other applicable
-jurisdiction, (b) if the reader is located in or ordinarily resident in
-a country or territory subject to comprehensive sanctions administered
-by the U.S. Office of Foreign Assets Control (OFAC), or (c) if the
-reader is or is working on behalf of a Specially Designated National
-(SDN) or a person subject to similar blocking or denied party
-prohibitions.
-
-The reader should be aware that U.S. export control and sanctions laws prohibit 
-U.S. persons (and other persons that are subject to such laws) from transacting 
-with persons in certain countries and territories or that are on the SDN list. 
-Accordingly, there is a risk to individuals that other persons using any of the 
-code contained in this repo, or a derivation thereof, may be sanctioned persons 
-and that transactions with such persons would be a violation of U.S. export 
-controls and sanctions law.
+We recommend checking out our [Gitbook](https://jito-foundation.gitbook.io/mev/jito-solana/building-the-software) for more detailed instructions on building and running Jito-Solana. 
diff --git a/SECURITY.md b/SECURITY.md
deleted file mode 100644
index 48326f1497..0000000000
--- a/SECURITY.md
+++ /dev/null
@@ -1,167 +0,0 @@
-# Security Policy
-
-1. [Reporting security problems](#reporting)
-4. [Security Bug Bounties](#bounty)
-2. [Incident Response Process](#process)
-
-<a name="reporting"></a>
-## Reporting security problems in the Solana Labs Validator Client
-
-**DO NOT CREATE A GITHUB ISSUE** to report a security problem.
-
-Instead please use this [Report a Vulnerability](https://github.com/solana-labs/solana/security/advisories/new) link.
-Provide a helpful title, detailed description of the vulnerability and an exploit
-proof-of-concept. Speculative submissions without proof-of-concept will be closed
-with no further consideration.
-
-If you haven't done so already, please **enable two-factor auth** in your GitHub account.
-
-Expect a response as fast as possible in the advisory, typically within 72 hours.
-
---
-
-If you do not receive a response in the advisory, send an email to
-security@solanalabs.com with the full URL of the advisory you have created.  DO NOT
-include attachments or provide detail sufficient for exploitation regarding the
-security issue in this email. **Only provide such details in the advisory**.
-
-If you do not receive a response from security@solanalabs.com please followup with
-the team directly. You can do this in the `#core-technology` channel of the
-[Solana Tech discord server](https://solana.com/discord), by pinging the `Solana Labs`
-role in the channel and referencing the fact that you submitted a security problem.
-
-<a name="process"></a>
-## Incident Response Process
-
-In case an incident is discovered or reported, the following process will be
-followed to contain, respond and remediate:
-
-### 1. Accept the new report
-In response a newly reported security problem, a member of the
-`solana-labs/admins` group will accept the report to turn it into a draft
-advisory.  The `solana-labs/security-incident-response` group should be added to
-the draft security advisory, and create a private fork of the repository (grey
-button towards the bottom of the page) if necessary.
-
-If the advisory is the result of an audit finding, follow the same process as above but add the auditor's github user(s) and begin the title with "[Audit]".
-
-If the report is out of scope, a member of the `solana-labs/admins` group will
-comment as such and then close the report.
-
-### 2. Triage
-Within the draft security advisory, discuss and determine the severity of the issue. If necessary, members of the solana-labs/security-incident-response group may add other github users to the advisory to assist.
-If it is determined that this not a critical network issue then the advisory should be closed and if more follow-up is required a normal Solana public github issue should be created.
-
-### 3. Prepare Fixes
-For the affected branches, typically all three (edge, beta and stable), prepare a fix for the issue and push them to the corresponding branch in the private repository associated with the draft security advisory.
-There is no CI available in the private repository so you must build from source and manually verify fixes.
-Code review from the reporter is ideal, as well as from multiple members of the core development team.
-
-### 4. Notify Security Group Validators
-Once an ETA is available for the fix, a member of the solana-labs/security-incident-response group should notify the validators so they can prepare for an update using the "Solana Red Alert" notification system.
-The teams are all over the world and it's critical to provide actionable information at the right time. Don't be the person that wakes everybody up at 2am when a fix won't be available for hours.
-
-### 5. Ship the patch
-Once the fix is accepted, a member of the solana-labs/security-incident-response group should prepare a single patch file for each affected branch. The commit title for the patch should only contain the advisory id, and not disclose any further details about the incident.
-Copy the patches to https://release.solana.com/ under a subdirectory named after the advisory id (example: https://release.solana.com/GHSA-hx59-f5g4-jghh/v1.4.patch). Contact a member of the solana-labs/admins group if you require access to release.solana.com
-Using the "Solana Red Alert" channel:
-    a) Notify validators that there's an issue and a patch will be provided in X minutes
-    b) If X minutes expires and there's no patch, notify of the delay and provide a new ETA
-    c) Provide links to patches of https://release.solana.com/ for each affected branch
-Validators can be expected to build the patch from source against the latest release for the affected branch.
-Since the software version will not change after the patch is applied, request that each validator notify in the existing channel once they've updated. Manually monitor the roll out until a sufficient amount of stake has updated - typically at least 33.3% or 66.6% depending on the issue.
-
-### 6. Public Disclosure and Release
-Once the fix has been deployed to the security group validators, the patches from the security advisory may be merged into the main source repository. A new official release for each affected branch should be shipped and all validators requested to upgrade as quickly as possible.
-
-### 7. Security Advisory Bounty Accounting and Cleanup
-If this issue is [eligible](#eligibility) for a bounty, prefix the title of the
-security advisory with one of the following, depending on the severity:
-- [Bounty Category: Critical: Loss of Funds]
-- [Bounty Category: Critical: Consensus / Safety Violations]
-- [Bounty Category: Critical: Liveness / Loss of Availability]
-- [Bounty Category: Critical: DoS Attacks]
-- [Bounty Category: Supply Chain Attacks]
-- [Bounty Category: RPC]
-
-Confirm with the reporter that they agree with the severity assessment, and discuss as required to reach a conclusion.
-
-We currently do not use the Github workflow to publish security advisories. Once the issue and fix have been disclosed, and a bounty category is assessed if appropriate, the GitHub security advisory is no longer needed and can be closed.
-
-<a name="bounty"></a>
-## Security Bug Bounties
-The Solana Foundation offer bounties for critical Solana security issues. Please
-see below for more details. Either a demonstration or a valid bug report is all
-that's necessary to submit a bug bounty. A patch to fix the issue isn't
-required.
-
-#### Loss of Funds:
-$2,000,000 USD in locked SOL tokens (locked for 12 months)
-* Theft of funds without users signature from any account
-* Theft of funds without users interaction in system, token, stake, vote programs
-* Theft of funds that requires users signature - creating a vote program that drains the delegated stakes.
-
-#### Consensus/Safety Violations:
-$1,000,000 USD in locked SOL tokens (locked for 12 months)
-* Consensus safety violation
-* Tricking a validator to accept an optimistic confirmation or rooted slot without a double vote, etc.
-
-#### Liveness / Loss of Availability:
-$400,000 USD in locked SOL tokens (locked for 12 months)
-* Whereby consensus halts and requires human intervention
-* Eclipse attacks,
-* Remote attacks that partition the network,
-
-#### DoS Attacks:
-$100,000 USD in locked SOL tokens (locked for 12 months)
-* Remote resource exaustion via Non-RPC protocols
-
-#### Supply Chain Attacks:
-$100,000 USD in locked SOL tokens (locked for 12 months)
-* Non-social attacks against source code change management, automated testing, release build, release publication and release hosting infrastructure of the monorepo.
-
-#### RPC DoS/Crashes:
-$5,000 USD in locked SOL tokens (locked for 12 months)
-* RPC attacks
-
-### Out of Scope:
-The following components are out of scope for the bounty program
-* Metrics: `/metrics` in the monorepo as well as https://metrics.solana.com
-* Any encrypted credentials, auth tokens, etc. checked into the repo
-* Bugs in dependencies. Please take them upstream!
-* Attacks that require social engineering
-* Any undeveloped automated tooling (scanners, etc) results. (OK with developed PoC)
-* Any asset whose source code does not exist in this repository (including, but not limited
-to, any and all web properties not explicitly listed on this page)
-
-### Eligibility:
-* Submissions _MUST_ include an exploit proof-of-concept to be considered eligible
-* The participant submitting the bug report shall follow the process outlined within this document
-* Valid exploits can be eligible even if they are not successfully executed on a public cluster
-* Multiple submissions for the same class of exploit are still eligible for compensation, though may be compensated at a lower rate, however these will be assessed on a case-by-case basis
-* Participants must complete KYC and sign the participation agreement here when the registrations are open https://solana.foundation/kyc. Security exploits will still be assessed and open for submission at all times. This needs only be done prior to distribution of tokens.
-
-### Duplicate Reports
-Compensation for duplicative reports will be split among reporters with first to report taking priority using the following equation
-```
-R: total reports
-ri: report priority
-bi: bounty share
-
-bi = 2 ^ (R - ri) / ((2^R) - 1)
-```
-#### Bounty Split Examples
-| total reports | priority | share  |   | total reports | priority | share  |   | total reports | priority | share  |
-| ------------- | -------- | -----: | - | ------------- | -------- | -----: | - | ------------- | -------- | -----: |
-| 1             | 1        | 100%   |   | 2             | 1        | 66.67% |   | 5             | 1        | 51.61% |
-|               |          |        |   | 2             | 2        | 33.33% |   | 5             | 2        | 25.81% |
-| 4             | 1        | 53.33% |   |               |          |        |   | 5             | 3        | 12.90% |
-| 4             | 2        | 26.67% |   | 3             | 1        | 57.14% |   | 5             | 4        |  6.45% |
-| 4             | 3        | 13.33% |   | 3             | 2        | 28.57% |   | 5             | 5        |  3.23% |
-| 4             | 4        |  6.67% |   | 3             | 3        | 14.29% |   |               |          |        |
-
-### Payment of Bug Bounties:
-* Bounties are currently awarded on a rolling/weekly basis and paid out within 30 days upon receipt of an invoice.
-* The SOL/USD conversion rate used for payments is the market price of SOL (denominated in USD) at the end of the day the invoice is submitted by the researcher.
-* The reference for this price is the Closing Price given by Coingecko.com on that date given here: https://www.coingecko.com/en/coins/solana/historical_data/usd#panel
-* Bug bounties that are paid out in SOL are paid to stake accounts with a lockup expiring 12 months from the date of delivery of SOL.
diff --git a/anchor b/anchor
new file mode 160000
index 0000000000..4f52f41cbe
--- /dev/null
+++ b/anchor
@@ -0,0 +1 @@
+Subproject commit 4f52f41cbeafb77d85c7b712516dfbeb5b86dd5f
diff --git a/banking-bench/src/main.rs b/banking-bench/src/main.rs
index f817df75e5..5ac679bf16 100644
--- a/banking-bench/src/main.rs
+++ b/banking-bench/src/main.rs
@@ -9,6 +9,7 @@ use {
     solana_core::{
         banking_stage::BankingStage,
         banking_trace::{BankingPacketBatch, BankingTracer, BANKING_TRACE_DIR_DEFAULT_BYTE_LIMIT},
+        bundle_stage::bundle_account_locker::BundleAccountLocker,
     },
     solana_gossip::cluster_info::{ClusterInfo, Node},
     solana_ledger::{
@@ -36,6 +37,7 @@ use {
     solana_streamer::socket::SocketAddrSpace,
     solana_tpu_client::tpu_client::DEFAULT_TPU_CONNECTION_POOL_SIZE,
     std::{
+        collections::HashSet,
         sync::{atomic::Ordering, Arc, RwLock},
         thread::sleep,
         time::{Duration, Instant},
@@ -57,9 +59,15 @@ fn check_txs(
     let now = Instant::now();
     let mut no_bank = false;
     loop {
-        if let Ok((_bank, (entry, _tick_height))) = receiver.recv_timeout(Duration::from_millis(10))
+        if let Ok(WorkingBankEntry {
+            bank: _,
+            entries_ticks,
+        }) = receiver.recv_timeout(Duration::from_millis(10))
         {
-            total += entry.transactions.len();
+            total += entries_ticks
+                .iter()
+                .map(|e| e.0.transactions.len())
+                .sum::<usize>();
         }
         if total >= ref_tx_count {
             break;
@@ -459,6 +467,8 @@ fn main() {
             Arc::new(connection_cache),
             bank_forks.clone(),
             &Arc::new(PrioritizationFeeCache::new(0u64)),
+            HashSet::default(),
+            BundleAccountLocker::default(),
         );
 
         // This is so that the signal_receiver does not go out of scope after the closure.
diff --git a/banks-server/Cargo.toml b/banks-server/Cargo.toml
index 4c29dcd30d..ffa0ac7ceb 100644
--- a/banks-server/Cargo.toml
+++ b/banks-server/Cargo.toml
@@ -15,12 +15,17 @@ crossbeam-channel = { workspace = true }
 futures = { workspace = true }
 solana-banks-interface = { workspace = true }
 solana-client = { workspace = true }
+solana-gossip = { workspace = true }
 solana-runtime = { workspace = true }
 solana-sdk = { workspace = true }
 solana-send-transaction-service = { workspace = true }
 tarpc = { workspace = true, features = ["full"] }
 tokio = { workspace = true, features = ["full"] }
 tokio-serde = { workspace = true, features = ["bincode"] }
+tokio-stream = { workspace = true }
+
+[dev-dependencies]
+solana-streamer = { workspace = true }
 
 [lib]
 crate-type = ["lib"]
diff --git a/banks-server/src/banks_server.rs b/banks-server/src/banks_server.rs
index cee47e8108..0eb057b2fc 100644
--- a/banks-server/src/banks_server.rs
+++ b/banks-server/src/banks_server.rs
@@ -8,6 +8,7 @@ use {
         TransactionSimulationDetails, TransactionStatus,
     },
     solana_client::connection_cache::ConnectionCache,
+    solana_gossip::cluster_info::ClusterInfo,
     solana_runtime::{
         bank::{Bank, TransactionExecutionResult, TransactionSimulationResult},
         bank_forks::BankForks,
@@ -439,7 +440,7 @@ pub async fn start_local_server(
 
 pub async fn start_tcp_server(
     listen_addr: SocketAddr,
-    tpu_addr: SocketAddr,
+    cluster_info: Arc<ClusterInfo>,
     bank_forks: Arc<RwLock<BankForks>>,
     block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
     connection_cache: Arc<ConnectionCache>,
@@ -464,7 +465,7 @@ pub async fn start_tcp_server(
             let (sender, receiver) = unbounded();
 
             SendTransactionService::new::<NullTpuInfo>(
-                tpu_addr,
+                cluster_info.clone(),
                 &bank_forks,
                 None,
                 receiver,
diff --git a/bootstrap b/bootstrap
new file mode 100755
index 0000000000..4f8955e5ca
--- /dev/null
+++ b/bootstrap
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+set -eu
+
+BANK_HASH=$(cargo run --release --bin solana-ledger-tool -- -l config/bootstrap-validator bank-hash)
+
+# increase max file handle limit
+ulimit -Hn 1000000
+
+# if above fails, run:
+# sudo bash -c 'echo "*               hard    nofile          1000000" >> /etc/security/limits.conf'
+
+# NOTE: make sure tip-payment and tip-distribution program are deployed using the correct pubkeys
+RUST_LOG=INFO,solana_core::bundle_stage=DEBUG \
+  NDEBUG=1 ./multinode-demo/bootstrap-validator.sh \
+  --wait-for-supermajority 0 \
+  --expected-bank-hash "$BANK_HASH" \
+  --block-engine-address http://127.0.0.1:1003 \
+  --block-engine-auth-service-address http://127.0.0.1:1005 \
+  --relayer-auth-service-address http://127.0.0.1:11226 \
+  --relayer-address http://127.0.0.1:11226 \
+  --rpc-pubsub-enable-block-subscription \
+  --enable-rpc-transaction-history \
+  --tip-payment-program-pubkey T1pyyaTNZsKv2WcRAB8oVnk93mLJw2XzjtVYqCsaHqt \
+  --tip-distribution-program-pubkey 4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7 \
+  --commission-bps 0 \
+  --shred-receiver-address 127.0.0.1:1002 \
+  --trust-relayer-packets \
+  --trust-block-engine-packets
diff --git a/bundle/Cargo.toml b/bundle/Cargo.toml
new file mode 100644
index 0000000000..e8bdd2cb4d
--- /dev/null
+++ b/bundle/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "solana-bundle"
+description = "Library related to handling bundles"
+documentation = "https://docs.rs/solana-bundle"
+readme = "../README.md"
+version = { workspace = true }
+authors = { workspace = true }
+repository = { workspace = true }
+homepage = { workspace = true }
+license = { workspace = true }
+edition = { workspace = true }
+
+[dependencies]
+anchor-lang = { workspace = true }
+itertools = { workspace = true }
+log = { workspace = true }
+serde = { workspace = true }
+solana-ledger = { workspace = true }
+solana-logger = { workspace = true }
+solana-measure = { workspace = true }
+solana-poh = { workspace = true }
+solana-program-runtime = { workspace = true }
+solana-runtime = { workspace = true }
+solana-sdk = { workspace = true }
+solana-transaction-status = { workspace = true }
+thiserror = { workspace = true }
+
+[dev-dependencies]
+assert_matches = { workspace = true }
+solana-logger = { workspace = true }
+
+[lib]
+crate-type = ["lib"]
+name = "solana_bundle"
diff --git a/bundle/src/bundle_execution.rs b/bundle/src/bundle_execution.rs
new file mode 100644
index 0000000000..48a78194c2
--- /dev/null
+++ b/bundle/src/bundle_execution.rs
@@ -0,0 +1,1186 @@
+use {
+    itertools::izip,
+    log::*,
+    solana_ledger::token_balances::collect_token_balances,
+    solana_measure::{measure::Measure, measure_us},
+    solana_program_runtime::timings::ExecuteTimings,
+    solana_runtime::{
+        account_overrides::AccountOverrides,
+        accounts::TransactionLoadResult,
+        bank::{
+            Bank, LoadAndExecuteTransactionsOutput, TransactionBalances, TransactionExecutionResult,
+        },
+        transaction_batch::TransactionBatch,
+    },
+    solana_sdk::{
+        account::AccountSharedData,
+        bundle::SanitizedBundle,
+        pubkey::Pubkey,
+        saturating_add_assign,
+        signature::Signature,
+        transaction::{SanitizedTransaction, TransactionError, VersionedTransaction},
+    },
+    solana_transaction_status::{token_balances::TransactionTokenBalances, PreBalanceInfo},
+    std::{
+        cmp::{max, min},
+        time::{Duration, Instant},
+    },
+    thiserror::Error,
+};
+
+#[derive(Clone, Default)]
+pub struct BundleExecutionMetrics {
+    pub num_retries: u64,
+    pub collect_balances_us: u64,
+    pub load_execute_us: u64,
+    pub collect_pre_post_accounts_us: u64,
+    pub cache_accounts_us: u64,
+    pub execute_timings: ExecuteTimings,
+}
+
+/// Contains the results from executing each TransactionBatch with a final result associated with it
+/// Note that if !result.is_ok(), bundle_transaction_results will not contain the output for every transaction.
+pub struct LoadAndExecuteBundleOutput<'a> {
+    bundle_transaction_results: Vec<BundleTransactionsOutput<'a>>,
+    result: LoadAndExecuteBundleResult<()>,
+    metrics: BundleExecutionMetrics,
+}
+
+impl<'a> LoadAndExecuteBundleOutput<'a> {
+    pub fn executed_ok(&self) -> bool {
+        self.result.is_ok()
+    }
+
+    pub fn result(&self) -> &LoadAndExecuteBundleResult<()> {
+        &self.result
+    }
+
+    pub fn bundle_transaction_results_mut(&mut self) -> &'a mut [BundleTransactionsOutput] {
+        &mut self.bundle_transaction_results
+    }
+
+    pub fn bundle_transaction_results(&self) -> &'a [BundleTransactionsOutput] {
+        &self.bundle_transaction_results
+    }
+
+    pub fn executed_transaction_batches(&self) -> Vec<Vec<VersionedTransaction>> {
+        self.bundle_transaction_results
+            .iter()
+            .map(|br| br.executed_versioned_transactions())
+            .collect()
+    }
+
+    pub fn metrics(&self) -> BundleExecutionMetrics {
+        self.metrics.clone()
+    }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum LoadAndExecuteBundleError {
+    #[error("Bundle execution timed out")]
+    ProcessingTimeExceeded(Duration),
+
+    #[error(
+        "A transaction in the bundle encountered a lock error: [signature={:?}, transaction_error={:?}]",
+        signature,
+        transaction_error
+    )]
+    LockError {
+        signature: Signature,
+        transaction_error: TransactionError,
+    },
+
+    #[error(
+        "A transaction in the bundle failed to execute: [signature={:?}, execution_result={:?}",
+        signature,
+        execution_result
+    )]
+    TransactionError {
+        signature: Signature,
+        // Box reduces the size between variants in the Error
+        execution_result: Box<TransactionExecutionResult>,
+    },
+
+    #[error("Invalid pre or post accounts")]
+    InvalidPreOrPostAccounts,
+}
+
+pub struct BundleTransactionsOutput<'a> {
+    transactions: &'a [SanitizedTransaction],
+    load_and_execute_transactions_output: LoadAndExecuteTransactionsOutput,
+    pre_balance_info: PreBalanceInfo,
+    post_balance_info: (TransactionBalances, TransactionTokenBalances),
+    // the length of the outer vector should be the same as transactions.len()
+    // for indices that didn't get executed, expect a None.
+    pre_tx_execution_accounts: Vec<Option<Vec<(Pubkey, AccountSharedData)>>>,
+    post_tx_execution_accounts: Vec<Option<Vec<(Pubkey, AccountSharedData)>>>,
+}
+
+impl<'a> BundleTransactionsOutput<'a> {
+    pub fn executed_versioned_transactions(&self) -> Vec<VersionedTransaction> {
+        self.transactions
+            .iter()
+            .zip(
+                self.load_and_execute_transactions_output
+                    .execution_results
+                    .iter(),
+            )
+            .filter_map(|(tx, exec_result)| {
+                exec_result
+                    .was_executed()
+                    .then_some(tx.to_versioned_transaction())
+            })
+            .collect()
+    }
+
+    pub fn executed_transactions(&self) -> Vec<&'a SanitizedTransaction> {
+        self.transactions
+            .iter()
+            .zip(
+                self.load_and_execute_transactions_output
+                    .execution_results
+                    .iter(),
+            )
+            .filter_map(|(tx, exec_result)| exec_result.was_executed().then_some(tx))
+            .collect()
+    }
+
+    pub fn load_and_execute_transactions_output(&self) -> &LoadAndExecuteTransactionsOutput {
+        &self.load_and_execute_transactions_output
+    }
+
+    pub fn transactions(&self) -> &[SanitizedTransaction] {
+        self.transactions
+    }
+
+    pub fn loaded_transactions_mut(&mut self) -> &mut [TransactionLoadResult] {
+        &mut self
+            .load_and_execute_transactions_output
+            .loaded_transactions
+    }
+
+    pub fn execution_results(&self) -> &[TransactionExecutionResult] {
+        &self.load_and_execute_transactions_output.execution_results
+    }
+
+    pub fn pre_balance_info(&mut self) -> &mut PreBalanceInfo {
+        &mut self.pre_balance_info
+    }
+
+    pub fn post_balance_info(&self) -> &(TransactionBalances, TransactionTokenBalances) {
+        &self.post_balance_info
+    }
+
+    pub fn pre_tx_execution_accounts(&self) -> &Vec<Option<Vec<(Pubkey, AccountSharedData)>>> {
+        &self.pre_tx_execution_accounts
+    }
+
+    pub fn post_tx_execution_accounts(&self) -> &Vec<Option<Vec<(Pubkey, AccountSharedData)>>> {
+        &self.post_tx_execution_accounts
+    }
+}
+
+pub type LoadAndExecuteBundleResult<T> = Result<T, LoadAndExecuteBundleError>;
+
+/// Return an Error if a transaction was executed and reverted
+/// NOTE: `execution_results` are zipped with `sanitized_txs` so it's expected a sanitized tx at
+/// position i has a corresponding execution result at position i within the `execution_results`
+/// slice
+pub fn check_bundle_execution_results<'a>(
+    execution_results: &'a [TransactionExecutionResult],
+    sanitized_txs: &'a [SanitizedTransaction],
+) -> Result<(), (&'a SanitizedTransaction, &'a TransactionExecutionResult)> {
+    for (exec_results, sanitized_tx) in execution_results.iter().zip(sanitized_txs) {
+        match exec_results {
+            TransactionExecutionResult::Executed { details, .. } => {
+                if details.status.is_err() {
+                    return Err((sanitized_tx, exec_results));
+                }
+            }
+            TransactionExecutionResult::NotExecuted(e) => {
+                if !matches!(e, TransactionError::AccountInUse) {
+                    return Err((sanitized_tx, exec_results));
+                }
+            }
+        }
+    }
+    Ok(())
+}
+
+/// Executing a bundle is somewhat complicated compared to executing single transactions. In order to
+/// avoid duplicate logic for execution and simulation, this function can be leveraged.
+///
+/// Assumptions for the caller:
+/// - all transactions were signed properly
+/// - user has deduplicated transactions inside the bundle
+///
+/// TODO (LB):
+/// - given a bundle with 3 transactions that write lock the following accounts: [A, B, C], on failure of B
+///   we should add in the BundleTransactionsOutput of A and C and return the error for B.
+#[allow(clippy::too_many_arguments)]
+pub fn load_and_execute_bundle<'a>(
+    bank: &Bank,
+    bundle: &'a SanitizedBundle,
+    // Max blockhash age
+    max_age: usize,
+    // Upper bound on execution time for a bundle
+    max_processing_time: &Duration,
+    // Execution data logging
+    enable_cpi_recording: bool,
+    enable_log_recording: bool,
+    enable_return_data_recording: bool,
+    enable_balance_recording: bool,
+    log_messages_bytes_limit: &Option<usize>,
+    // simulation will not use the Bank's account locks when building the TransactionBatch
+    // if simulating on an unfrozen bank, this is helpful to avoid stalling replay and use whatever
+    // state the accounts are in at the current time
+    is_simulation: bool,
+    account_overrides: Option<&mut AccountOverrides>,
+    // these must be the same length as the bundle's transactions
+    // allows one to read account state before and after execution of each transaction in the bundle
+    // will use AccountsOverride + Bank
+    pre_execution_accounts: &Vec<Option<Vec<Pubkey>>>,
+    post_execution_accounts: &Vec<Option<Vec<Pubkey>>>,
+) -> LoadAndExecuteBundleOutput<'a> {
+    if pre_execution_accounts.len() != post_execution_accounts.len()
+        || post_execution_accounts.len() != bundle.transactions.len()
+    {
+        return LoadAndExecuteBundleOutput {
+            bundle_transaction_results: vec![],
+            result: Err(LoadAndExecuteBundleError::InvalidPreOrPostAccounts),
+            metrics: BundleExecutionMetrics::default(),
+        };
+    }
+    let mut binding = AccountOverrides::default();
+    let account_overrides = account_overrides.unwrap_or(&mut binding);
+
+    let mut chunk_start = 0;
+    let start_time = Instant::now();
+
+    let mut bundle_transaction_results = vec![];
+    let mut metrics = BundleExecutionMetrics::default();
+
+    while chunk_start != bundle.transactions.len() {
+        if start_time.elapsed() > *max_processing_time {
+            trace!("bundle: {} took too long to execute", bundle.bundle_id);
+            return LoadAndExecuteBundleOutput {
+                bundle_transaction_results,
+                metrics,
+                result: Err(LoadAndExecuteBundleError::ProcessingTimeExceeded(
+                    start_time.elapsed(),
+                )),
+            };
+        }
+
+        let chunk_end = min(bundle.transactions.len(), chunk_start.saturating_add(128));
+        let chunk = &bundle.transactions[chunk_start..chunk_end];
+
+        // Note: these batches are dropped after execution and before record/commit, which is atypical
+        // compared to BankingStage which holds account locks until record + commit to avoid race conditions with
+        // other BankingStage threads. However, the caller of this method, BundleConsumer, will use BundleAccountLocks
+        // to hold RW locks across all transactions in a bundle until its processed.
+        let batch = if is_simulation {
+            bank.prepare_sequential_sanitized_batch_with_results_for_simulation(chunk)
+        } else {
+            bank.prepare_sequential_sanitized_batch_with_results(chunk)
+        };
+
+        debug!(
+            "bundle: {} batch num locks ok: {}",
+            bundle.bundle_id,
+            batch.lock_results().iter().filter(|lr| lr.is_ok()).count()
+        );
+
+        // Ensures that bundle lock results only return either:
+        // Ok(()) | Err(TransactionError::AccountInUse)
+        // If the error isn't one of those, the
+        if let Some((transaction, lock_failure)) = batch.check_bundle_lock_results() {
+            debug!(
+                "bundle: {} lock error; signature: {} error: {}",
+                bundle.bundle_id,
+                transaction.signature(),
+                lock_failure
+            );
+            return LoadAndExecuteBundleOutput {
+                bundle_transaction_results,
+                metrics,
+                result: Err(LoadAndExecuteBundleError::LockError {
+                    signature: *transaction.signature(),
+                    transaction_error: lock_failure.clone(),
+                }),
+            };
+        }
+
+        let mut pre_balance_info = PreBalanceInfo::default();
+        let (_, collect_balances_us) = measure_us!({
+            if enable_balance_recording {
+                pre_balance_info.native =
+                    bank.collect_balances_with_cache(&batch, Some(account_overrides));
+                pre_balance_info.token = collect_token_balances(
+                    bank,
+                    &batch,
+                    &mut pre_balance_info.mint_decimals,
+                    Some(account_overrides),
+                );
+            }
+        });
+        saturating_add_assign!(metrics.collect_balances_us, collect_balances_us);
+
+        let end = min(
+            chunk_start.saturating_add(batch.sanitized_transactions().len()),
+            pre_execution_accounts.len(),
+        );
+
+        let m = Measure::start("accounts");
+        let accounts_requested = &pre_execution_accounts[chunk_start..end];
+        let pre_tx_execution_accounts =
+            get_account_transactions(bank, account_overrides, accounts_requested, &batch);
+        saturating_add_assign!(metrics.collect_pre_post_accounts_us, m.end_as_us());
+
+        let (mut load_and_execute_transactions_output, load_execute_us) = measure_us!(bank
+            .load_and_execute_transactions(
+                &batch,
+                max_age,
+                enable_cpi_recording,
+                enable_log_recording,
+                enable_return_data_recording,
+                &mut metrics.execute_timings,
+                Some(account_overrides),
+                *log_messages_bytes_limit,
+            ));
+        debug!(
+            "bundle id: {} loaded_transactions: {:?}",
+            bundle.bundle_id, load_and_execute_transactions_output.loaded_transactions
+        );
+        saturating_add_assign!(metrics.load_execute_us, load_execute_us);
+
+        // All transactions within a bundle are expected to be executable + not fail
+        // If there's any transactions that executed and failed or didn't execute due to
+        // unexpected failures (not locking related), bail out of bundle execution early.
+        if let Err((failing_tx, exec_result)) = check_bundle_execution_results(
+            load_and_execute_transactions_output
+                .execution_results
+                .as_slice(),
+            batch.sanitized_transactions(),
+        ) {
+            // TODO (LB): we should try to return partial results here for successful bundles in a parallel batch.
+            //  given a bundle that write locks the following accounts [[A], [B], [C]]
+            //  when B fails, we could return the execution results for A and C, but leave B out.
+            //  however, if we have bundle that write locks accounts [[A_1], [A_2], [B], [C]] and B fails
+            //  we'll get the results for A_1 but not [A_2], [B], [C] due to the way this loop executes.
+            debug!(
+                "bundle: {} execution error; signature: {} error: {:?}",
+                bundle.bundle_id,
+                failing_tx.signature(),
+                exec_result
+            );
+            return LoadAndExecuteBundleOutput {
+                bundle_transaction_results,
+                metrics,
+                result: Err(LoadAndExecuteBundleError::TransactionError {
+                    signature: *failing_tx.signature(),
+                    execution_result: Box::new(exec_result.clone()),
+                }),
+            };
+        }
+
+        // If none of the transactions were executed, most likely an AccountInUse error
+        // need to retry to ensure that all transactions in the bundle are executed.
+        if !load_and_execute_transactions_output
+            .execution_results
+            .iter()
+            .any(|r| r.was_executed())
+        {
+            saturating_add_assign!(metrics.num_retries, 1);
+            debug!(
+                "bundle: {} no transaction executed, retrying",
+                bundle.bundle_id
+            );
+            continue;
+        }
+
+        // Cache accounts so next iterations of loop can load cached state instead of using
+        // AccountsDB, which will contain stale account state because results aren't committed
+        // to the bank yet.
+        // NOTE: Bank::collect_accounts_to_store does not handle any state changes related to
+        // failed, non-nonce transactions.
+        let m = Measure::start("cache");
+        let accounts = bank.collect_accounts_to_store(
+            batch.sanitized_transactions(),
+            &load_and_execute_transactions_output.execution_results,
+            &mut load_and_execute_transactions_output.loaded_transactions,
+        );
+        for (pubkey, data) in accounts {
+            account_overrides.set_account(pubkey, Some(data.clone()));
+        }
+        saturating_add_assign!(metrics.cache_accounts_us, m.end_as_us());
+
+        let end = max(
+            chunk_start.saturating_add(batch.sanitized_transactions().len()),
+            post_execution_accounts.len(),
+        );
+
+        let m = Measure::start("accounts");
+        let accounts_requested = &post_execution_accounts[chunk_start..end];
+        let post_tx_execution_accounts =
+            get_account_transactions(bank, account_overrides, accounts_requested, &batch);
+        saturating_add_assign!(metrics.collect_pre_post_accounts_us, m.end_as_us());
+
+        let ((post_balances, post_token_balances), collect_balances_us) =
+            measure_us!(if enable_balance_recording {
+                let post_balances =
+                    bank.collect_balances_with_cache(&batch, Some(account_overrides));
+                let post_token_balances = collect_token_balances(
+                    bank,
+                    &batch,
+                    &mut pre_balance_info.mint_decimals,
+                    Some(account_overrides),
+                );
+                (post_balances, post_token_balances)
+            } else {
+                (
+                    TransactionBalances::default(),
+                    TransactionTokenBalances::default(),
+                )
+            });
+        saturating_add_assign!(metrics.collect_balances_us, collect_balances_us);
+
+        let processing_end = batch.lock_results().iter().position(|lr| lr.is_err());
+        if let Some(end) = processing_end {
+            chunk_start = chunk_start.saturating_add(end);
+        } else {
+            chunk_start = chunk_end;
+        }
+
+        bundle_transaction_results.push(BundleTransactionsOutput {
+            transactions: chunk,
+            load_and_execute_transactions_output,
+            pre_balance_info,
+            post_balance_info: (post_balances, post_token_balances),
+            pre_tx_execution_accounts,
+            post_tx_execution_accounts,
+        });
+    }
+
+    LoadAndExecuteBundleOutput {
+        bundle_transaction_results,
+        metrics,
+        result: Ok(()),
+    }
+}
+
+fn get_account_transactions(
+    bank: &Bank,
+    account_overrides: &mut AccountOverrides,
+    accounts: &[Option<Vec<Pubkey>>],
+    batch: &TransactionBatch,
+) -> Vec<Option<Vec<(Pubkey, AccountSharedData)>>> {
+    let iter = izip!(batch.lock_results().iter(), accounts.iter());
+
+    iter.map(|(lock_result, accounts_requested)| {
+        if lock_result.is_ok() {
+            accounts_requested.as_ref().map(|accounts_requested| {
+                accounts_requested
+                    .iter()
+                    .map(|a| match account_overrides.get(a) {
+                        None => (*a, bank.get_account(a).unwrap_or_default()),
+                        Some(data) => (*a, data.clone()),
+                    })
+                    .collect()
+            })
+        } else {
+            None
+        }
+    })
+    .collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use {
+        crate::bundle_execution::{load_and_execute_bundle, LoadAndExecuteBundleError},
+        assert_matches::assert_matches,
+        solana_ledger::genesis_utils::create_genesis_config,
+        solana_runtime::{bank::Bank, genesis_utils::GenesisConfigInfo},
+        solana_sdk::{
+            bundle::{derive_bundle_id_from_sanitized_transactions, SanitizedBundle},
+            clock::MAX_PROCESSING_AGE,
+            pubkey::Pubkey,
+            signature::{Keypair, Signer},
+            system_transaction::transfer,
+            transaction::{SanitizedTransaction, Transaction, TransactionError},
+        },
+        std::{
+            sync::{Arc, Barrier},
+            thread::{sleep, spawn},
+            time::Duration,
+        },
+    };
+
+    const MAX_PROCESSING_TIME: Duration = Duration::from_secs(1);
+    const LOG_MESSAGE_BYTES_LIMITS: Option<usize> = Some(100_000);
+    const MINT_AMOUNT_LAMPORTS: u64 = 1_000_000;
+
+    fn create_simple_test_bank(lamports: u64) -> (GenesisConfigInfo, Arc<Bank>) {
+        let genesis_config_info = create_genesis_config(lamports);
+        let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
+        (genesis_config_info, bank)
+    }
+
+    fn make_bundle(txs: &[Transaction]) -> SanitizedBundle {
+        let transactions: Vec<_> = txs
+            .iter()
+            .map(|tx| SanitizedTransaction::try_from_legacy_transaction(tx.clone()).unwrap())
+            .collect();
+
+        let bundle_id = derive_bundle_id_from_sanitized_transactions(&transactions);
+
+        SanitizedBundle {
+            transactions,
+            bundle_id,
+        }
+    }
+
+    fn find_account_index(tx: &Transaction, account: &Pubkey) -> Option<usize> {
+        tx.message
+            .account_keys
+            .iter()
+            .position(|pubkey| account == pubkey)
+    }
+
+    /// A single, valid bundle shall execute successfully and return the correct BundleTransactionsOutput content
+    #[test]
+    fn test_single_transaction_bundle_success() {
+        const TRANSFER_AMOUNT: u64 = 1_000;
+        let (genesis_config_info, bank) = create_simple_test_bank(MINT_AMOUNT_LAMPORTS);
+        let lamports_per_signature = bank
+            .get_lamports_per_signature_for_blockhash(&genesis_config_info.genesis_config.hash())
+            .unwrap();
+
+        let kp = Keypair::new();
+        let transactions = vec![transfer(
+            &genesis_config_info.mint_keypair,
+            &kp.pubkey(),
+            TRANSFER_AMOUNT,
+            genesis_config_info.genesis_config.hash(),
+        )];
+        let bundle = make_bundle(&transactions);
+        let default_accounts = vec![None; bundle.transactions.len()];
+
+        let execution_result = load_and_execute_bundle(
+            &bank,
+            &bundle,
+            MAX_PROCESSING_AGE,
+            &MAX_PROCESSING_TIME,
+            true,
+            true,
+            true,
+            true,
+            &LOG_MESSAGE_BYTES_LIMITS,
+            false,
+            None,
+            &default_accounts,
+            &default_accounts,
+        );
+
+        // make sure the bundle succeeded
+        assert!(execution_result.result.is_ok());
+
+        // check to make sure there was one batch returned with one transaction that was the same that was put in
+        assert_eq!(execution_result.bundle_transaction_results.len(), 1);
+        let tx_result = execution_result.bundle_transaction_results.get(0).unwrap();
+        assert_eq!(tx_result.transactions.len(), 1);
+        assert_eq!(tx_result.transactions[0], bundle.transactions[0]);
+
+        // make sure the transaction executed successfully
+        assert_eq!(
+            tx_result
+                .load_and_execute_transactions_output
+                .execution_results
+                .len(),
+            1
+        );
+        let execution_result = tx_result
+            .load_and_execute_transactions_output
+            .execution_results
+            .get(0)
+            .unwrap();
+        assert!(execution_result.was_executed());
+        assert!(execution_result.was_executed_successfully());
+
+        // Make sure the post-balances are correct
+        assert_eq!(tx_result.pre_balance_info.native.len(), 1);
+        let post_tx_sol_balances = tx_result.post_balance_info.0.get(0).unwrap();
+
+        let minter_message_index =
+            find_account_index(&transactions[0], &genesis_config_info.mint_keypair.pubkey())
+                .unwrap();
+        let receiver_message_index = find_account_index(&transactions[0], &kp.pubkey()).unwrap();
+
+        assert_eq!(
+            post_tx_sol_balances[minter_message_index],
+            MINT_AMOUNT_LAMPORTS - lamports_per_signature - TRANSFER_AMOUNT
+        );
+        assert_eq!(
+            post_tx_sol_balances[receiver_message_index],
+            TRANSFER_AMOUNT
+        );
+    }
+
+    /// Test a simple failure
+    #[test]
+    fn test_single_transaction_bundle_fail() {
+        const TRANSFER_AMOUNT: u64 = 1_000;
+        let (genesis_config_info, bank) = create_simple_test_bank(MINT_AMOUNT_LAMPORTS);
+
+        // kp has no funds, transfer will fail
+        let kp = Keypair::new();
+        let transactions = vec![transfer(
+            &kp,
+            &kp.pubkey(),
+            TRANSFER_AMOUNT,
+            genesis_config_info.genesis_config.hash(),
+        )];
+        let bundle = make_bundle(&transactions);
+
+        let default_accounts = vec![None; bundle.transactions.len()];
+        let execution_result = load_and_execute_bundle(
+            &bank,
+            &bundle,
+            MAX_PROCESSING_AGE,
+            &MAX_PROCESSING_TIME,
+            true,
+            true,
+            true,
+            true,
+            &LOG_MESSAGE_BYTES_LIMITS,
+            false,
+            None,
+            &default_accounts,
+            &default_accounts,
+        );
+
+        assert_eq!(execution_result.bundle_transaction_results.len(), 0);
+
+        assert!(execution_result.result.is_err());
+
+        match execution_result.result.unwrap_err() {
+            LoadAndExecuteBundleError::ProcessingTimeExceeded(_)
+            | LoadAndExecuteBundleError::LockError { .. }
+            | LoadAndExecuteBundleError::InvalidPreOrPostAccounts => {
+                unreachable!();
+            }
+            LoadAndExecuteBundleError::TransactionError {
+                signature,
+                execution_result,
+            } => {
+                assert_eq!(signature, *bundle.transactions[0].signature());
+                assert!(!execution_result.was_executed());
+            }
+        }
+    }
+
+    /// Tests a multi-tx bundle that succeeds. Checks the returned results
+    #[test]
+    fn test_multi_transaction_bundle_success() {
+        const TRANSFER_AMOUNT_1: u64 = 100_000;
+        const TRANSFER_AMOUNT_2: u64 = 50_000;
+        const TRANSFER_AMOUNT_3: u64 = 10_000;
+        let (genesis_config_info, bank) = create_simple_test_bank(MINT_AMOUNT_LAMPORTS);
+        let lamports_per_signature = bank
+            .get_lamports_per_signature_for_blockhash(&genesis_config_info.genesis_config.hash())
+            .unwrap();
+
+        // mint transfers 100k to 1
+        // 1 transfers 50k to 2
+        // 2 transfers 10k to 3
+        // should get executed in 3 batches [[1], [2], [3]]
+        let kp1 = Keypair::new();
+        let kp2 = Keypair::new();
+        let kp3 = Keypair::new();
+        let transactions = vec![
+            transfer(
+                &genesis_config_info.mint_keypair,
+                &kp1.pubkey(),
+                TRANSFER_AMOUNT_1,
+                genesis_config_info.genesis_config.hash(),
+            ),
+            transfer(
+                &kp1,
+                &kp2.pubkey(),
+                TRANSFER_AMOUNT_2,
+                genesis_config_info.genesis_config.hash(),
+            ),
+            transfer(
+                &kp2,
+                &kp3.pubkey(),
+                TRANSFER_AMOUNT_3,
+                genesis_config_info.genesis_config.hash(),
+            ),
+        ];
+        let bundle = make_bundle(&transactions);
+
+        let default_accounts = vec![None; bundle.transactions.len()];
+        let execution_result = load_and_execute_bundle(
+            &bank,
+            &bundle,
+            MAX_PROCESSING_AGE,
+            &MAX_PROCESSING_TIME,
+            true,
+            true,
+            true,
+            true,
+            &LOG_MESSAGE_BYTES_LIMITS,
+            false,
+            None,
+            &default_accounts,
+            &default_accounts,
+        );
+
+        assert!(execution_result.result.is_ok());
+        assert_eq!(execution_result.bundle_transaction_results.len(), 3);
+
+        // first batch contains the first tx that was executed
+        assert_eq!(
+            execution_result.bundle_transaction_results[0].transactions,
+            bundle.transactions
+        );
+        assert_eq!(
+            execution_result.bundle_transaction_results[0]
+                .load_and_execute_transactions_output
+                .execution_results
+                .len(),
+            3
+        );
+        assert!(execution_result.bundle_transaction_results[0]
+            .load_and_execute_transactions_output
+            .execution_results[0]
+            .was_executed_successfully());
+        assert_eq!(
+            execution_result.bundle_transaction_results[0]
+                .load_and_execute_transactions_output
+                .execution_results[1]
+                .flattened_result(),
+            Err(TransactionError::AccountInUse)
+        );
+        assert_eq!(
+            execution_result.bundle_transaction_results[0]
+                .load_and_execute_transactions_output
+                .execution_results[2]
+                .flattened_result(),
+            Err(TransactionError::AccountInUse)
+        );
+        assert_eq!(
+            execution_result.bundle_transaction_results[0]
+                .pre_balance_info
+                .native
+                .len(),
+            3
+        );
+        assert_eq!(
+            execution_result.bundle_transaction_results[0]
+                .post_balance_info
+                .0
+                .len(),
+            3
+        );
+
+        let minter_index =
+            find_account_index(&transactions[0], &genesis_config_info.mint_keypair.pubkey())
+                .unwrap();
+        let kp1_index = find_account_index(&transactions[0], &kp1.pubkey()).unwrap();
+
+        assert_eq!(
+            execution_result.bundle_transaction_results[0]
+                .post_balance_info
+                .0[0][minter_index],
+            MINT_AMOUNT_LAMPORTS - lamports_per_signature - TRANSFER_AMOUNT_1
+        );
+
+        assert_eq!(
+            execution_result.bundle_transaction_results[0]
+                .post_balance_info
+                .0[0][kp1_index],
+            TRANSFER_AMOUNT_1
+        );
+
+        // in the second batch, the second transaction was executed
+        assert_eq!(
+            execution_result.bundle_transaction_results[1]
+                .transactions
+                .to_owned(),
+            bundle.transactions[1..]
+        );
+        assert_eq!(
+            execution_result.bundle_transaction_results[1]
+                .load_and_execute_transactions_output
+                .execution_results
+                .len(),
+            2
+        );
+        assert!(execution_result.bundle_transaction_results[1]
+            .load_and_execute_transactions_output
+            .execution_results[0]
+            .was_executed_successfully());
+        assert_eq!(
+            execution_result.bundle_transaction_results[1]
+                .load_and_execute_transactions_output
+                .execution_results[1]
+                .flattened_result(),
+            Err(TransactionError::AccountInUse)
+        );
+
+        assert_eq!(
+            execution_result.bundle_transaction_results[1]
+                .pre_balance_info
+                .native
+                .len(),
+            2
+        );
+        assert_eq!(
+            execution_result.bundle_transaction_results[1]
+                .post_balance_info
+                .0
+                .len(),
+            2
+        );
+
+        let kp1_index = find_account_index(&transactions[1], &kp1.pubkey()).unwrap();
+        let kp2_index = find_account_index(&transactions[1], &kp2.pubkey()).unwrap();
+
+        assert_eq!(
+            execution_result.bundle_transaction_results[1]
+                .post_balance_info
+                .0[0][kp1_index],
+            TRANSFER_AMOUNT_1 - lamports_per_signature - TRANSFER_AMOUNT_2
+        );
+
+        assert_eq!(
+            execution_result.bundle_transaction_results[1]
+                .post_balance_info
+                .0[0][kp2_index],
+            TRANSFER_AMOUNT_2
+        );
+
+        // in the third batch, the third transaction was executed
+        assert_eq!(
+            execution_result.bundle_transaction_results[2]
+                .transactions
+                .to_owned(),
+            bundle.transactions[2..]
+        );
+        assert_eq!(
+            execution_result.bundle_transaction_results[2]
+                .load_and_execute_transactions_output
+                .execution_results
+                .len(),
+            1
+        );
+        assert!(execution_result.bundle_transaction_results[2]
+            .load_and_execute_transactions_output
+            .execution_results[0]
+            .was_executed_successfully());
+
+        assert_eq!(
+            execution_result.bundle_transaction_results[2]
+                .pre_balance_info
+                .native
+                .len(),
+            1
+        );
+        assert_eq!(
+            execution_result.bundle_transaction_results[2]
+                .post_balance_info
+                .0
+                .len(),
+            1
+        );
+
+        let kp2_index = find_account_index(&transactions[2], &kp2.pubkey()).unwrap();
+        let kp3_index = find_account_index(&transactions[2], &kp3.pubkey()).unwrap();
+
+        assert_eq!(
+            execution_result.bundle_transaction_results[2]
+                .post_balance_info
+                .0[0][kp2_index],
+            TRANSFER_AMOUNT_2 - lamports_per_signature - TRANSFER_AMOUNT_3
+        );
+
+        assert_eq!(
+            execution_result.bundle_transaction_results[2]
+                .post_balance_info
+                .0[0][kp3_index],
+            TRANSFER_AMOUNT_3
+        );
+    }
+
+    /// Tests a multi-tx bundle with the middle transaction failing.
+    #[test]
+    fn test_multi_transaction_bundle_fails() {
+        let (genesis_config_info, bank) = create_simple_test_bank(MINT_AMOUNT_LAMPORTS);
+
+        let kp1 = Keypair::new();
+        let kp2 = Keypair::new();
+        let kp3 = Keypair::new();
+        let transactions = vec![
+            transfer(
+                &genesis_config_info.mint_keypair,
+                &kp1.pubkey(),
+                100_000,
+                genesis_config_info.genesis_config.hash(),
+            ),
+            transfer(
+                &kp2,
+                &kp3.pubkey(),
+                100_000,
+                genesis_config_info.genesis_config.hash(),
+            ),
+            transfer(
+                &kp1,
+                &kp2.pubkey(),
+                100_000,
+                genesis_config_info.genesis_config.hash(),
+            ),
+        ];
+        let bundle = make_bundle(&transactions);
+
+        let default_accounts = vec![None; bundle.transactions.len()];
+        let execution_result = load_and_execute_bundle(
+            &bank,
+            &bundle,
+            MAX_PROCESSING_AGE,
+            &MAX_PROCESSING_TIME,
+            true,
+            true,
+            true,
+            true,
+            &LOG_MESSAGE_BYTES_LIMITS,
+            false,
+            None,
+            &default_accounts,
+            &default_accounts,
+        );
+        match execution_result.result.as_ref().unwrap_err() {
+            LoadAndExecuteBundleError::ProcessingTimeExceeded(_)
+            | LoadAndExecuteBundleError::LockError { .. }
+            | LoadAndExecuteBundleError::InvalidPreOrPostAccounts => {
+                unreachable!();
+            }
+
+            LoadAndExecuteBundleError::TransactionError {
+                signature,
+                execution_result: tx_failure,
+            } => {
+                assert_eq!(signature, bundle.transactions[1].signature());
+                assert_eq!(
+                    tx_failure.flattened_result(),
+                    Err(TransactionError::AccountNotFound)
+                );
+                assert_eq!(execution_result.bundle_transaction_results().len(), 0);
+            }
+        }
+    }
+
+    /// Tests that when the max processing time is exceeded, the bundle is an error
+    #[test]
+    fn test_bundle_max_processing_time_exceeded() {
+        let (genesis_config_info, bank) = create_simple_test_bank(MINT_AMOUNT_LAMPORTS);
+
+        let kp = Keypair::new();
+        let transactions = vec![transfer(
+            &genesis_config_info.mint_keypair,
+            &kp.pubkey(),
+            1,
+            genesis_config_info.genesis_config.hash(),
+        )];
+        let bundle = make_bundle(&transactions);
+
+        let locked_transfer = vec![SanitizedTransaction::from_transaction_for_tests(transfer(
+            &genesis_config_info.mint_keypair,
+            &kp.pubkey(),
+            2,
+            genesis_config_info.genesis_config.hash(),
+        ))];
+
+        // locks it and prevents execution bc write lock on genesis_config_info.mint_keypair + kp.pubkey() held
+        let _batch = bank.prepare_sanitized_batch(&locked_transfer);
+
+        let default = vec![None; bundle.transactions.len()];
+        let result = load_and_execute_bundle(
+            &bank,
+            &bundle,
+            MAX_PROCESSING_AGE,
+            &Duration::from_millis(100),
+            false,
+            false,
+            false,
+            false,
+            &None,
+            false,
+            None,
+            &default,
+            &default,
+        );
+        assert_matches!(
+            result.result,
+            Err(LoadAndExecuteBundleError::ProcessingTimeExceeded(_))
+        );
+    }
+
+    #[test]
+    fn test_simulate_bundle_with_locked_account_works() {
+        let (genesis_config_info, bank) = create_simple_test_bank(MINT_AMOUNT_LAMPORTS);
+
+        let kp = Keypair::new();
+        let transactions = vec![transfer(
+            &genesis_config_info.mint_keypair,
+            &kp.pubkey(),
+            1,
+            genesis_config_info.genesis_config.hash(),
+        )];
+        let bundle = make_bundle(&transactions);
+
+        let locked_transfer = vec![SanitizedTransaction::from_transaction_for_tests(transfer(
+            &genesis_config_info.mint_keypair,
+            &kp.pubkey(),
+            2,
+            genesis_config_info.genesis_config.hash(),
+        ))];
+
+        let _batch = bank.prepare_sanitized_batch(&locked_transfer);
+
+        // simulation ignores account locks so you can simulate bundles on unfrozen banks
+        let default = vec![None; bundle.transactions.len()];
+        let result = load_and_execute_bundle(
+            &bank,
+            &bundle,
+            MAX_PROCESSING_AGE,
+            &Duration::from_millis(100),
+            false,
+            false,
+            false,
+            false,
+            &None,
+            true,
+            None,
+            &default,
+            &default,
+        );
+        assert!(result.result.is_ok());
+    }
+
+    /// Creates a multi-tx bundle and temporarily locks the accounts for one of the transactions in a bundle.
+    /// Ensures the result is what's expected
+    #[test]
+    fn test_bundle_works_with_released_account_locks() {
+        let (genesis_config_info, bank) = create_simple_test_bank(MINT_AMOUNT_LAMPORTS);
+        let barrier = Arc::new(Barrier::new(2));
+
+        let kp = Keypair::new();
+
+        let transactions = vec![transfer(
+            &genesis_config_info.mint_keypair,
+            &kp.pubkey(),
+            1,
+            genesis_config_info.genesis_config.hash(),
+        )];
+        let bundle = Arc::new(make_bundle(&transactions));
+
+        let locked_transfer = vec![SanitizedTransaction::from_transaction_for_tests(transfer(
+            &genesis_config_info.mint_keypair,
+            &kp.pubkey(),
+            2,
+            genesis_config_info.genesis_config.hash(),
+        ))];
+
+        // background thread locks the accounts for a bit then unlocks them
+        let thread = {
+            let barrier = barrier.clone();
+            let bank = bank.clone();
+            spawn(move || {
+                let batch = bank.prepare_sanitized_batch(&locked_transfer);
+                barrier.wait();
+                sleep(Duration::from_millis(500));
+                drop(batch);
+            })
+        };
+
+        let _ = barrier.wait();
+
+        // load_and_execute_bundle should spin for a bit then process after the 500ms sleep is over
+        let default = vec![None; bundle.transactions.len()];
+        let result = load_and_execute_bundle(
+            &bank,
+            &bundle,
+            MAX_PROCESSING_AGE,
+            &Duration::from_secs(2),
+            false,
+            false,
+            false,
+            false,
+            &None,
+            false,
+            None,
+            &default,
+            &default,
+        );
+        assert!(result.result.is_ok());
+
+        thread.join().unwrap();
+    }
+
+    /// Tests that when the max processing time is exceeded, the bundle is an error
+    #[test]
+    fn test_bundle_bad_pre_post_accounts() {
+        let (genesis_config_info, bank) = create_simple_test_bank(MINT_AMOUNT_LAMPORTS);
+
+        let kp = Keypair::new();
+        let transactions = vec![transfer(
+            &genesis_config_info.mint_keypair,
+            &kp.pubkey(),
+            1,
+            genesis_config_info.genesis_config.hash(),
+        )];
+        let bundle = make_bundle(&transactions);
+
+        let result = load_and_execute_bundle(
+            &bank,
+            &bundle,
+            MAX_PROCESSING_AGE,
+            &Duration::from_millis(100),
+            false,
+            false,
+            false,
+            false,
+            &None,
+            false,
+            None,
+            &vec![None; 2],
+            &vec![None; bundle.transactions.len()],
+        );
+        assert_matches!(
+            result.result,
+            Err(LoadAndExecuteBundleError::InvalidPreOrPostAccounts)
+        );
+
+        let result = load_and_execute_bundle(
+            &bank,
+            &bundle,
+            MAX_PROCESSING_AGE,
+            &Duration::from_millis(100),
+            false,
+            false,
+            false,
+            false,
+            &None,
+            false,
+            None,
+            &vec![None; bundle.transactions.len()],
+            &vec![None; 2],
+        );
+        assert_matches!(
+            result.result,
+            Err(LoadAndExecuteBundleError::InvalidPreOrPostAccounts)
+        );
+    }
+}
diff --git a/bundle/src/lib.rs b/bundle/src/lib.rs
new file mode 100644
index 0000000000..a93e0d3d17
--- /dev/null
+++ b/bundle/src/lib.rs
@@ -0,0 +1,60 @@
+use {
+    crate::bundle_execution::LoadAndExecuteBundleError,
+    anchor_lang::error::Error,
+    serde::{Deserialize, Serialize},
+    solana_poh::poh_recorder::PohRecorderError,
+    solana_sdk::pubkey::Pubkey,
+    thiserror::Error,
+};
+
+pub mod bundle_execution;
+
+#[derive(Error, Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub enum TipError {
+    #[error("account is missing from bank: {0}")]
+    AccountMissing(Pubkey),
+
+    #[error("Anchor error: {0}")]
+    AnchorError(String),
+
+    #[error("Lock error")]
+    LockError,
+
+    #[error("Error executing initialize programs")]
+    InitializeProgramsError,
+
+    #[error("Error cranking tip programs")]
+    CrankTipError,
+}
+
+impl From<anchor_lang::error::Error> for TipError {
+    fn from(anchor_err: Error) -> Self {
+        match anchor_err {
+            Error::AnchorError(e) => Self::AnchorError(e.error_msg),
+            Error::ProgramError(e) => Self::AnchorError(e.to_string()),
+        }
+    }
+}
+
+pub type BundleExecutionResult<T> = Result<T, BundleExecutionError>;
+
+#[derive(Error, Debug, Clone)]
+pub enum BundleExecutionError {
+    #[error("The bank has hit the max allotted time for processing transactions")]
+    BankProcessingTimeLimitReached,
+
+    #[error("The bundle exceeds the cost model")]
+    ExceedsCostModel,
+
+    #[error("Runtime error while executing the bundle: {0}")]
+    TransactionFailure(#[from] LoadAndExecuteBundleError),
+
+    #[error("Error locking bundle because a transaction is malformed")]
+    LockError,
+
+    #[error("PoH record error: {0}")]
+    PohRecordError(#[from] PohRecorderError),
+
+    #[error("Tip payment error {0}")]
+    TipError(#[from] TipError),
+}
diff --git a/ci/buildkite-pipeline-in-disk.sh b/ci/buildkite-pipeline-in-disk.sh
index 113b009aa4..6b41beda32 100644
--- a/ci/buildkite-pipeline-in-disk.sh
+++ b/ci/buildkite-pipeline-in-disk.sh
@@ -292,7 +292,7 @@ if [[ -n $BUILDKITE_TAG ]]; then
     "https://github.com/solana-labs/solana/releases/$BUILDKITE_TAG"
 
   # Jump directly to the secondary build to publish release artifacts quickly
-  trigger_secondary_step
+#  trigger_secondary_step
   exit 0
 fi
 
@@ -320,5 +320,5 @@ fi
 start_pipeline "Push pipeline for ${BUILDKITE_BRANCH:-?unknown branch?}"
 pull_or_push_steps
 wait_step
-trigger_secondary_step
+#trigger_secondary_step
 exit 0
diff --git a/ci/buildkite-pipeline.sh b/ci/buildkite-pipeline.sh
index d7467f98b7..78b92d26c1 100755
--- a/ci/buildkite-pipeline.sh
+++ b/ci/buildkite-pipeline.sh
@@ -309,7 +309,7 @@ if [[ -n $BUILDKITE_TAG ]]; then
     "https://github.com/solana-labs/solana/releases/$BUILDKITE_TAG"
 
   # Jump directly to the secondary build to publish release artifacts quickly
-  trigger_secondary_step
+#  trigger_secondary_step
   exit 0
 fi
 
@@ -337,5 +337,5 @@ fi
 start_pipeline "Push pipeline for ${BUILDKITE_BRANCH:-?unknown branch?}"
 pull_or_push_steps
 wait_step
-trigger_secondary_step
+#trigger_secondary_step
 exit 0
diff --git a/ci/channel-info.sh b/ci/channel-info.sh
index c82806454d..101583307f 100755
--- a/ci/channel-info.sh
+++ b/ci/channel-info.sh
@@ -11,7 +11,7 @@ here="$(dirname "$0")"
 # shellcheck source=ci/semver_bash/semver.sh
 source "$here"/semver_bash/semver.sh
 
-remote=https://github.com/solana-labs/solana.git
+remote=https://github.com/jito-foundation/jito-solana.git
 
 # Fetch all vX.Y.Z tags
 #
diff --git a/ci/check-crates.sh b/ci/check-crates.sh
index 655504ea11..d6a9ad9c39 100755
--- a/ci/check-crates.sh
+++ b/ci/check-crates.sh
@@ -31,6 +31,9 @@ printf "%s\n" "${files[@]}"
 error_count=0
 for file in "${files[@]}"; do
   read -r crate_name package_publish workspace < <(toml get "$file" . | jq -r '(.package.name | tostring)+" "+(.package.publish | tostring)+" "+(.workspace | tostring)')
+  if [ "$crate_name" == "solana-bundle" ]; then
+    continue
+  fi
   echo "=== $crate_name ($file) ==="
 
   if [[ $package_publish = 'false' ]]; then
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 46eae11c4a..3ce235ba1a 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -14,6 +14,7 @@ edition = { workspace = true }
 codecov = { repository = "solana-labs/solana", branch = "master", service = "github" }
 
 [dependencies]
+anchor-lang = { workspace = true }
 base64 = { workspace = true }
 bincode = { workspace = true }
 bs58 = { workspace = true }
@@ -24,11 +25,16 @@ eager = { workspace = true }
 etcd-client = { workspace = true, features = ["tls"] }
 histogram = { workspace = true }
 itertools = { workspace = true }
+jito-protos = { workspace = true }
+jito-tip-distribution = { workspace = true }
+jito-tip-payment = { workspace = true }
 lazy_static = { workspace = true }
 log = { workspace = true }
 lru = { workspace = true }
 min-max-heap = { workspace = true }
 num_enum = { workspace = true }
+prost = { workspace = true }
+prost-types = { workspace = true }
 rand = { workspace = true }
 rand_chacha = { workspace = true }
 rayon = { workspace = true }
@@ -37,6 +43,7 @@ serde = { workspace = true }
 serde_derive = { workspace = true }
 solana-address-lookup-table-program = { workspace = true }
 solana-bloom = { workspace = true }
+solana-bundle = { workspace = true }
 solana-client = { workspace = true }
 solana-entry = { workspace = true }
 solana-frozen-abi = { workspace = true }
@@ -54,6 +61,7 @@ solana-rayon-threadlimit = { workspace = true }
 solana-rpc = { workspace = true }
 solana-rpc-client-api = { workspace = true }
 solana-runtime = { workspace = true }
+solana-runtime-plugin = { workspace = true }
 solana-sdk = { workspace = true }
 solana-send-transaction-service = { workspace = true }
 solana-streamer = { workspace = true }
@@ -67,6 +75,7 @@ sys-info = { workspace = true }
 tempfile = { workspace = true }
 thiserror = { workspace = true }
 tokio = { version = "~1.14.1", features = ["full"] }
+tonic = { workspace = true }
 trees = { workspace = true }
 
 [dev-dependencies]
@@ -75,8 +84,10 @@ matches = { workspace = true }
 raptorq = { workspace = true }
 serde_json = { workspace = true }
 serial_test = { workspace = true }
+solana-bundle = { workspace = true }
 solana-logger = { workspace = true }
 solana-program-runtime = { workspace = true }
+solana-program-test = { workspace = true }
 solana-stake-program = { workspace = true }
 static_assertions = { workspace = true }
 systemstat = { workspace = true }
@@ -87,6 +98,7 @@ sysctl = { workspace = true }
 
 [build-dependencies]
 rustc_version = { workspace = true }
+tonic-build = { workspace = true }
 
 [[bench]]
 name = "banking_stage"
diff --git a/core/benches/banking_stage.rs b/core/benches/banking_stage.rs
index a6254292a5..22932e01a4 100644
--- a/core/benches/banking_stage.rs
+++ b/core/benches/banking_stage.rs
@@ -14,6 +14,7 @@ use {
             committer::Committer, consumer::Consumer, BankingStage, BankingStageStats,
         },
         banking_trace::{BankingPacketBatch, BankingTracer},
+        bundle_stage::bundle_account_locker::BundleAccountLocker,
         leader_slot_banking_stage_metrics::LeaderSlotMetricsTracker,
         qos_service::QosService,
         unprocessed_packet_batches::*,
@@ -50,6 +51,7 @@ use {
         vote_state::VoteStateUpdate, vote_transaction::new_vote_state_update_transaction,
     },
     std::{
+        collections::HashSet,
         iter::repeat_with,
         sync::{atomic::Ordering, Arc, RwLock},
         time::{Duration, Instant},
@@ -61,8 +63,15 @@ fn check_txs(receiver: &Arc<Receiver<WorkingBankEntry>>, ref_tx_count: usize) {
     let mut total = 0;
     let now = Instant::now();
     loop {
-        if let Ok((_bank, (entry, _tick_height))) = receiver.recv_timeout(Duration::new(1, 0)) {
-            total += entry.transactions.len();
+        if let Ok(WorkingBankEntry {
+            bank: _,
+            entries_ticks,
+        }) = receiver.recv_timeout(Duration::new(1, 0))
+        {
+            total += entries_ticks
+                .iter()
+                .map(|e| e.0.transactions.len())
+                .sum::<usize>();
         }
         if total >= ref_tx_count {
             break;
@@ -105,7 +114,14 @@ fn bench_consume_buffered(bencher: &mut Bencher) {
         );
         let (s, _r) = unbounded();
         let committer = Committer::new(None, s, Arc::new(PrioritizationFeeCache::new(0u64)));
-        let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+        let consumer = Consumer::new(
+            committer,
+            recorder,
+            QosService::new(1),
+            None,
+            HashSet::default(),
+            BundleAccountLocker::default(),
+        );
         // This tests the performance of buffering packets.
         // If the packet buffers are copied, performance will be poor.
         bencher.iter(move || {
@@ -299,7 +315,9 @@ fn bench_banking(bencher: &mut Bencher, tx_type: TransactionType) {
             None,
             Arc::new(ConnectionCache::new("connection_cache_test")),
             bank_forks,
-            &Arc::new(PrioritizationFeeCache::new(0u64)),
+            &Arc::new(PrioritizationFeeCache::default()),
+            HashSet::default(),
+            BundleAccountLocker::default(),
         );
 
         let chunk_len = verified.len() / CHUNKS;
diff --git a/core/benches/cluster_info.rs b/core/benches/cluster_info.rs
index 04eb85c2dd..46da7fd03b 100644
--- a/core/benches/cluster_info.rs
+++ b/core/benches/cluster_info.rs
@@ -79,6 +79,7 @@ fn broadcast_shreds_bench(bencher: &mut Bencher) {
             &cluster_info,
             &bank_forks,
             &SocketAddrSpace::Unspecified,
+            &None,
         )
         .unwrap();
     });
diff --git a/core/benches/consumer.rs b/core/benches/consumer.rs
index d6f908c577..875297a131 100644
--- a/core/benches/consumer.rs
+++ b/core/benches/consumer.rs
@@ -9,15 +9,15 @@ use {
     },
     solana_core::{
         banking_stage::{committer::Committer, consumer::Consumer},
+        bundle_stage::bundle_account_locker::BundleAccountLocker,
         qos_service::QosService,
     },
-    solana_entry::entry::Entry,
     solana_ledger::{
         blockstore::Blockstore,
         genesis_utils::{create_genesis_config, GenesisConfigInfo},
     },
     solana_poh::{
-        poh_recorder::{create_test_recorder, PohRecorder},
+        poh_recorder::{create_test_recorder, PohRecorder, WorkingBankEntry},
         poh_service::PohService,
     },
     solana_runtime::bank::Bank,
@@ -26,9 +26,12 @@ use {
         signer::Signer, stake_history::Epoch, system_program, system_transaction,
         transaction::SanitizedTransaction,
     },
-    std::sync::{
-        atomic::{AtomicBool, Ordering},
-        Arc, RwLock,
+    std::{
+        collections::HashSet,
+        sync::{
+            atomic::{AtomicBool, Ordering},
+            Arc, RwLock,
+        },
     },
     tempfile::TempDir,
     test::Bencher,
@@ -81,7 +84,14 @@ fn create_consumer(poh_recorder: &RwLock<PohRecorder>) -> Consumer {
     let (replay_vote_sender, _replay_vote_receiver) = unbounded();
     let committer = Committer::new(None, replay_vote_sender, Arc::default());
     let transaction_recorder = poh_recorder.read().unwrap().new_recorder();
-    Consumer::new(committer, transaction_recorder, QosService::new(0), None)
+    Consumer::new(
+        committer,
+        transaction_recorder,
+        QosService::new(0),
+        None,
+        HashSet::default(),
+        BundleAccountLocker::default(),
+    )
 }
 
 struct BenchFrame {
@@ -90,7 +100,7 @@ struct BenchFrame {
     exit: Arc<AtomicBool>,
     poh_recorder: Arc<RwLock<PohRecorder>>,
     poh_service: PohService,
-    signal_receiver: Receiver<(Arc<Bank>, (Entry, u64))>,
+    signal_receiver: Receiver<WorkingBankEntry>,
 }
 
 fn setup(apply_cost_tracker_during_replay: bool) -> BenchFrame {
diff --git a/core/benches/proto_to_packet.rs b/core/benches/proto_to_packet.rs
new file mode 100644
index 0000000000..87f85f9c7f
--- /dev/null
+++ b/core/benches/proto_to_packet.rs
@@ -0,0 +1,56 @@
+#![feature(test)]
+
+extern crate test;
+
+use {
+    jito_protos::proto::packet::{
+        Meta as PbMeta, Packet as PbPacket, PacketBatch, PacketFlags as PbFlags,
+    },
+    solana_core::proto_packet_to_packet,
+    solana_sdk::packet::{Packet, PACKET_DATA_SIZE},
+    std::iter::repeat,
+    test::{black_box, Bencher},
+};
+
+fn get_proto_packet(i: u8) -> PbPacket {
+    PbPacket {
+        data: repeat(i).take(PACKET_DATA_SIZE).collect(),
+        meta: Some(PbMeta {
+            size: PACKET_DATA_SIZE as u64,
+            addr: "255.255.255.255:65535".to_string(),
+            port: 65535,
+            flags: Some(PbFlags {
+                discard: false,
+                forwarded: false,
+                repair: false,
+                simple_vote_tx: false,
+                tracer_packet: false,
+            }),
+            sender_stake: 0,
+        }),
+    }
+}
+
+#[bench]
+fn bench_proto_to_packet(bencher: &mut Bencher) {
+    bencher.iter(|| {
+        black_box(proto_packet_to_packet(get_proto_packet(1)));
+    });
+}
+
+#[bench]
+fn bench_batch_list_to_packets(bencher: &mut Bencher) {
+    let packet_batch = PacketBatch {
+        packets: (0..128).map(get_proto_packet).collect(),
+    };
+
+    bencher.iter(|| {
+        black_box(
+            packet_batch
+                .packets
+                .iter()
+                .map(|p| proto_packet_to_packet(p.clone()))
+                .collect::<Vec<Packet>>(),
+        );
+    });
+}
diff --git a/core/benches/retransmit_stage.rs b/core/benches/retransmit_stage.rs
index 9af88ae880..440c5353a4 100644
--- a/core/benches/retransmit_stage.rs
+++ b/core/benches/retransmit_stage.rs
@@ -124,6 +124,7 @@ fn bench_retransmitter(bencher: &mut Bencher) {
         shreds_receiver,
         Arc::default(), // solana_rpc::max_slots::MaxSlots
         None,
+        Arc::new(RwLock::new(None)),
     );
 
     let mut index = 0;
diff --git a/core/src/accounts_hash_verifier.rs b/core/src/accounts_hash_verifier.rs
index eb2e7ba9d0..32d0c0bda7 100644
--- a/core/src/accounts_hash_verifier.rs
+++ b/core/src/accounts_hash_verifier.rs
@@ -75,7 +75,8 @@ impl AccountsHashVerifier {
                     )) = Self::get_next_accounts_package(
                         &accounts_package_sender,
                         &accounts_package_receiver,
-                    ) else {
+                    )
+                    else {
                         std::thread::sleep(LOOP_LIMITER);
                         continue;
                     };
@@ -302,7 +303,9 @@ impl AccountsHashVerifier {
                 (accounts_hash.into(), accounts_hash, None)
             }
             CalcAccountsHashFlavor::Incremental => {
-                let AccountsPackageType::Snapshot(SnapshotType::IncrementalSnapshot(base_slot)) = accounts_package.package_type else {
+                let AccountsPackageType::Snapshot(SnapshotType::IncrementalSnapshot(base_slot)) =
+                    accounts_package.package_type
+                else {
                     panic!("Calculating incremental accounts hash requires a base slot");
                 };
                 let (base_accounts_hash, base_capitalization) = accounts_package
diff --git a/core/src/admin_rpc_post_init.rs b/core/src/admin_rpc_post_init.rs
index 110e1f5aa4..7373ffd5b3 100644
--- a/core/src/admin_rpc_post_init.rs
+++ b/core/src/admin_rpc_post_init.rs
@@ -1,10 +1,12 @@
 use {
+    crate::proxy::{block_engine_stage::BlockEngineConfig, relayer_stage::RelayerConfig},
     solana_gossip::cluster_info::ClusterInfo,
     solana_runtime::bank_forks::BankForks,
     solana_sdk::pubkey::Pubkey,
     std::{
         collections::HashSet,
-        sync::{Arc, RwLock},
+        net::SocketAddr,
+        sync::{Arc, Mutex, RwLock},
     },
 };
 
@@ -14,4 +16,7 @@ pub struct AdminRpcRequestMetadataPostInit {
     pub bank_forks: Arc<RwLock<BankForks>>,
     pub vote_account: Pubkey,
     pub repair_whitelist: Arc<RwLock<HashSet<Pubkey>>>,
+    pub block_engine_config: Arc<Mutex<BlockEngineConfig>>,
+    pub relayer_config: Arc<Mutex<RelayerConfig>>,
+    pub shred_receiver_address: Arc<RwLock<Option<SocketAddr>>>,
 }
diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs
index dc75d24a53..337b4e40ea 100644
--- a/core/src/banking_stage.rs
+++ b/core/src/banking_stage.rs
@@ -12,6 +12,7 @@ use {
     crate::{
         banking_stage::committer::Committer,
         banking_trace::BankingPacketReceiver,
+        bundle_stage::bundle_account_locker::BundleAccountLocker,
         latest_unprocessed_votes::{LatestUnprocessedVotes, VoteSource},
         leader_slot_banking_stage_metrics::LeaderSlotMetricsTracker,
         qos_service::QosService,
@@ -31,9 +32,14 @@ use {
         bank_forks::BankForks, prioritization_fee_cache::PrioritizationFeeCache,
         vote_sender_types::ReplayVoteSender,
     },
-    solana_sdk::{feature_set::allow_votes_to_directly_update_vote_state, timing::AtomicInterval},
+    solana_sdk::{
+        feature_set::allow_votes_to_directly_update_vote_state, pubkey::Pubkey,
+        timing::AtomicInterval,
+    },
     std::{
-        cmp, env,
+        cmp,
+        collections::HashSet,
+        env,
         sync::{
             atomic::{AtomicU64, AtomicUsize, Ordering},
             Arc, RwLock,
@@ -45,7 +51,7 @@ use {
 
 pub mod committer;
 pub mod consumer;
-mod decision_maker;
+pub(crate) mod decision_maker;
 mod forwarder;
 mod packet_receiver;
 
@@ -311,6 +317,8 @@ impl BankingStage {
         connection_cache: Arc<ConnectionCache>,
         bank_forks: Arc<RwLock<BankForks>>,
         prioritization_fee_cache: &Arc<PrioritizationFeeCache>,
+        blacklisted_accounts: HashSet<Pubkey>,
+        bundle_account_locker: BundleAccountLocker,
     ) -> Self {
         Self::new_num_threads(
             cluster_info,
@@ -325,6 +333,8 @@ impl BankingStage {
             connection_cache,
             bank_forks,
             prioritization_fee_cache,
+            blacklisted_accounts,
+            bundle_account_locker,
         )
     }
 
@@ -342,6 +352,8 @@ impl BankingStage {
         connection_cache: Arc<ConnectionCache>,
         bank_forks: Arc<RwLock<BankForks>>,
         prioritization_fee_cache: &Arc<PrioritizationFeeCache>,
+        blacklisted_accounts: HashSet<Pubkey>,
+        bundle_account_locker: BundleAccountLocker,
     ) -> Self {
         assert!(num_threads >= MIN_TOTAL_THREADS);
         // Single thread to generate entries from many banks.
@@ -424,6 +436,8 @@ impl BankingStage {
                     poh_recorder.read().unwrap().new_recorder(),
                     QosService::new(id),
                     log_messages_bytes_limit,
+                    blacklisted_accounts.clone(),
+                    bundle_account_locker.clone(),
                 );
 
                 Builder::new()
@@ -584,7 +598,7 @@ mod tests {
         crate::banking_trace::{BankingPacketBatch, BankingTracer},
         crossbeam_channel::{unbounded, Receiver},
         itertools::Itertools,
-        solana_entry::entry::{Entry, EntrySlice},
+        solana_entry::entry::EntrySlice,
         solana_gossip::cluster_info::Node,
         solana_ledger::{
             blockstore::Blockstore,
@@ -598,6 +612,7 @@ mod tests {
         solana_poh::{
             poh_recorder::{
                 create_test_recorder, PohRecorderError, Record, RecordTransactionsSummary,
+                WorkingBankEntry,
             },
             poh_service::PohService,
         },
@@ -673,6 +688,8 @@ mod tests {
                 Arc::new(ConnectionCache::new("connection_cache_test")),
                 bank_forks,
                 &Arc::new(PrioritizationFeeCache::new(0u64)),
+                HashSet::default(),
+                BundleAccountLocker::default(),
             );
             drop(non_vote_sender);
             drop(tpu_vote_sender);
@@ -729,6 +746,8 @@ mod tests {
                 Arc::new(ConnectionCache::new("connection_cache_test")),
                 bank_forks,
                 &Arc::new(PrioritizationFeeCache::new(0u64)),
+                HashSet::default(),
+                BundleAccountLocker::default(),
             );
             trace!("sending bank");
             drop(non_vote_sender);
@@ -741,7 +760,12 @@ mod tests {
             trace!("getting entries");
             let entries: Vec<_> = entry_receiver
                 .iter()
-                .map(|(_bank, (entry, _tick_height))| entry)
+                .flat_map(
+                    |WorkingBankEntry {
+                         bank: _,
+                         entries_ticks,
+                     }| entries_ticks.into_iter().map(|(e, _)| e),
+                )
                 .collect();
             trace!("done");
             assert_eq!(entries.len(), genesis_config.ticks_per_slot as usize);
@@ -810,6 +834,8 @@ mod tests {
                 Arc::new(ConnectionCache::new("connection_cache_test")),
                 bank_forks,
                 &Arc::new(PrioritizationFeeCache::new(0u64)),
+                HashSet::default(),
+                BundleAccountLocker::default(),
             );
 
             // fund another account so we can send 2 good transactions in a single batch.
@@ -861,9 +887,14 @@ mod tests {
             bank.process_transaction(&fund_tx).unwrap();
             //receive entries + ticks
             loop {
-                let entries: Vec<Entry> = entry_receiver
+                let entries: Vec<_> = entry_receiver
                     .iter()
-                    .map(|(_bank, (entry, _tick_height))| entry)
+                    .flat_map(
+                        |WorkingBankEntry {
+                             bank: _,
+                             entries_ticks,
+                         }| entries_ticks.into_iter().map(|(e, _)| e),
+                    )
                     .collect();
 
                 assert!(entries.verify(&blockhash));
@@ -972,6 +1003,8 @@ mod tests {
                     Arc::new(ConnectionCache::new("connection_cache_test")),
                     bank_forks,
                     &Arc::new(PrioritizationFeeCache::new(0u64)),
+                    HashSet::default(),
+                    BundleAccountLocker::default(),
                 );
 
                 // wait for banking_stage to eat the packets
@@ -990,7 +1023,12 @@ mod tests {
             // check that the balance is what we expect.
             let entries: Vec<_> = entry_receiver
                 .iter()
-                .map(|(_bank, (entry, _tick_height))| entry)
+                .flat_map(
+                    |WorkingBankEntry {
+                         bank: _,
+                         entries_ticks,
+                     }| entries_ticks.into_iter().map(|(e, _)| e),
+                )
                 .collect();
 
             let bank = Bank::new_no_wallclock_throttle_for_tests(&genesis_config);
@@ -1051,15 +1089,19 @@ mod tests {
                 system_transaction::transfer(&keypair2, &pubkey2, 1, genesis_config.hash()).into(),
             ];
 
-            let _ = recorder.record_transactions(bank.slot(), txs.clone());
-            let (_bank, (entry, _tick_height)) = entry_receiver.recv().unwrap();
+            let _ = recorder.record_transactions(bank.slot(), vec![txs.clone()]);
+            let WorkingBankEntry {
+                bank,
+                entries_ticks,
+            } = entry_receiver.recv().unwrap();
+            let entry = &entries_ticks.get(0).unwrap().0;
             assert_eq!(entry.transactions, txs);
 
             // Once bank is set to a new bank (setting bank.slot() + 1 in record_transactions),
             // record_transactions should throw MaxHeightReached
             let next_slot = bank.slot() + 1;
             let RecordTransactionsSummary { result, .. } =
-                recorder.record_transactions(next_slot, txs);
+                recorder.record_transactions(next_slot, vec![txs]);
             assert_matches!(result, Err(PohRecorderError::MaxHeightReached));
             // Should receive nothing from PohRecorder b/c record failed
             assert!(entry_receiver.try_recv().is_err());
@@ -1166,6 +1208,8 @@ mod tests {
                 Arc::new(ConnectionCache::new("connection_cache_test")),
                 bank_forks,
                 &Arc::new(PrioritizationFeeCache::new(0u64)),
+                HashSet::default(),
+                BundleAccountLocker::default(),
             );
 
             let keypairs = (0..100).map(|_| Keypair::new()).collect_vec();
diff --git a/core/src/banking_stage/committer.rs b/core/src/banking_stage/committer.rs
index 81ae708d65..5925f64289 100644
--- a/core/src/banking_stage/committer.rs
+++ b/core/src/banking_stage/committer.rs
@@ -16,11 +16,9 @@ use {
         transaction_batch::TransactionBatch,
         vote_sender_types::ReplayVoteSender,
     },
-    solana_sdk::{pubkey::Pubkey, saturating_add_assign},
-    solana_transaction_status::{
-        token_balances::TransactionTokenBalancesSet, TransactionTokenBalance,
-    },
-    std::{collections::HashMap, sync::Arc},
+    solana_sdk::saturating_add_assign,
+    solana_transaction_status::{token_balances::TransactionTokenBalancesSet, PreBalanceInfo},
+    std::sync::Arc,
 };
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -29,13 +27,6 @@ pub enum CommitTransactionDetails {
     NotCommitted,
 }
 
-#[derive(Default)]
-pub(super) struct PreBalanceInfo {
-    pub native: Vec<Vec<u64>>,
-    pub token: Vec<Vec<TransactionTokenBalance>>,
-    pub mint_decimals: HashMap<Pubkey, u8>,
-}
-
 pub struct Committer {
     transaction_status_sender: Option<TransactionStatusSender>,
     replay_vote_sender: ReplayVoteSender,
@@ -144,7 +135,7 @@ impl Committer {
             let txs = batch.sanitized_transactions().to_vec();
             let post_balances = bank.collect_balances(batch);
             let post_token_balances =
-                collect_token_balances(bank, batch, &mut pre_balance_info.mint_decimals);
+                collect_token_balances(bank, batch, &mut pre_balance_info.mint_decimals, None);
             let mut transaction_index = starting_transaction_index.unwrap_or_default();
             let batch_transaction_indexes: Vec<_> = tx_results
                 .execution_results
diff --git a/core/src/banking_stage/consume_worker.rs b/core/src/banking_stage/consume_worker.rs
index 856b5ad6f2..396c755217 100644
--- a/core/src/banking_stage/consume_worker.rs
+++ b/core/src/banking_stage/consume_worker.rs
@@ -132,6 +132,7 @@ mod tests {
                 scheduler_messages::{TransactionBatchId, TransactionId},
                 tests::{create_slow_genesis_config, sanitize_transactions, simulate_poh},
             },
+            bundle_stage::bundle_account_locker::BundleAccountLocker,
             qos_service::QosService,
         },
         crossbeam_channel::unbounded,
@@ -149,6 +150,7 @@ mod tests {
             signature::Keypair, system_transaction,
         },
         std::{
+            collections::HashSet,
             sync::{atomic::AtomicBool, RwLock},
             thread::JoinHandle,
         },
@@ -206,7 +208,14 @@ mod tests {
             replay_vote_sender,
             Arc::new(PrioritizationFeeCache::new(0u64)),
         );
-        let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+        let consumer = Consumer::new(
+            committer,
+            recorder,
+            QosService::new(1),
+            None,
+            HashSet::default(),
+            BundleAccountLocker::default(),
+        );
 
         let (consume_sender, consume_receiver) = unbounded();
         let (consumed_sender, consumed_receiver) = unbounded();
diff --git a/core/src/banking_stage/consumer.rs b/core/src/banking_stage/consumer.rs
index 9ae1009672..79d3c1750b 100644
--- a/core/src/banking_stage/consumer.rs
+++ b/core/src/banking_stage/consumer.rs
@@ -4,7 +4,7 @@ use {
         BankingStageStats,
     },
     crate::{
-        banking_stage::committer::PreBalanceInfo,
+        bundle_stage::bundle_account_locker::BundleAccountLocker,
         immutable_deserialized_packet::ImmutableDeserializedPacket,
         leader_slot_banking_stage_metrics::{LeaderSlotMetricsTracker, ProcessTransactionsSummary},
         leader_slot_banking_stage_timing_metrics::LeaderExecuteAndCommitTimings,
@@ -18,7 +18,6 @@ use {
         BankStart, PohRecorderError, RecordTransactionsSummary, RecordTransactionsTimings,
         TransactionRecorder,
     },
-    solana_program_runtime::timings::ExecuteTimings,
     solana_runtime::{
         bank::{Bank, LoadAndExecuteTransactionsOutput, TransactionCheckResult},
         transaction_batch::TransactionBatch,
@@ -26,11 +25,15 @@ use {
     },
     solana_sdk::{
         clock::{Slot, FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET, MAX_PROCESSING_AGE},
-        feature_set, saturating_add_assign,
+        feature_set,
+        pubkey::Pubkey,
+        saturating_add_assign,
         timing::timestamp,
         transaction::{self, AddressLoader, SanitizedTransaction, TransactionError},
     },
+    solana_transaction_status::PreBalanceInfo,
     std::{
+        collections::HashSet,
         sync::{atomic::Ordering, Arc},
         time::Instant,
     },
@@ -70,6 +73,8 @@ pub struct Consumer {
     transaction_recorder: TransactionRecorder,
     qos_service: QosService,
     log_messages_bytes_limit: Option<usize>,
+    blacklisted_accounts: HashSet<Pubkey>,
+    bundle_account_locker: BundleAccountLocker,
 }
 
 impl Consumer {
@@ -78,12 +83,16 @@ impl Consumer {
         transaction_recorder: TransactionRecorder,
         qos_service: QosService,
         log_messages_bytes_limit: Option<usize>,
+        blacklisted_accounts: HashSet<Pubkey>,
+        bundle_account_locker: BundleAccountLocker,
     ) -> Self {
         Self {
             committer,
             transaction_recorder,
             qos_service,
             log_messages_bytes_limit,
+            blacklisted_accounts,
+            bundle_account_locker,
         }
     }
 
@@ -113,6 +122,7 @@ impl Consumer {
                     packets_to_process,
                 )
             },
+            &self.blacklisted_accounts,
         );
 
         if reached_end_of_slot {
@@ -443,20 +453,26 @@ impl Consumer {
             cost_model_us,
         ) = measure_us!(self.qos_service.select_and_accumulate_transaction_costs(
             bank,
+            &mut bank.write_cost_tracker().unwrap(),
             txs,
             pre_results
         ));
 
         // Only lock accounts for those transactions are selected for the block;
         // Once accounts are locked, other threads cannot encode transactions that will modify the
-        // same account state
+        // same account state.
+        // BundleAccountLocker is used to prevent race conditions with bundled transactions from bundle stage
+        let bundle_account_locks = self.bundle_account_locker.account_locks();
         let (batch, lock_us) = measure_us!(bank.prepare_sanitized_batch_with_results(
             txs,
             transaction_qos_cost_results.iter().map(|r| match r {
                 Ok(_cost) => Ok(()),
                 Err(err) => Err(err.clone()),
-            })
+            }),
+            &bundle_account_locks.read_locks(),
+            &bundle_account_locks.write_locks()
         ));
+        drop(bundle_account_locks);
 
         // retryable_txs includes AccountInUse, WouldExceedMaxBlockCostLimit
         // WouldExceedMaxAccountCostLimit, WouldExceedMaxVoteCostLimit
@@ -501,8 +517,9 @@ impl Consumer {
             .iter_mut()
             .for_each(|x| *x += chunk_offset);
 
-        let (cu, us) =
-            Self::accumulate_execute_units_and_time(&execute_and_commit_timings.execute_timings);
+        let (cu, us) = execute_and_commit_timings
+            .execute_timings
+            .accumulate_execute_units_and_time();
         self.qos_service.accumulate_actual_execute_cu(cu);
         self.qos_service.accumulate_actual_execute_time(us);
 
@@ -539,7 +556,7 @@ impl Consumer {
             if transaction_status_sender_enabled {
                 pre_balance_info.native = bank.collect_balances(batch);
                 pre_balance_info.token =
-                    collect_token_balances(bank, batch, &mut pre_balance_info.mint_decimals)
+                    collect_token_balances(bank, batch, &mut pre_balance_info.mint_decimals, None)
             }
         });
         execute_and_commit_timings.collect_balances_us = collect_balances_us;
@@ -588,7 +605,7 @@ impl Consumer {
 
         let (record_transactions_summary, record_us) = measure_us!(self
             .transaction_recorder
-            .record_transactions(bank.slot(), executed_transactions));
+            .record_transactions(bank.slot(), vec![executed_transactions]));
         execute_and_commit_timings.record_us = record_us;
 
         let RecordTransactionsSummary {
@@ -670,18 +687,6 @@ impl Consumer {
         }
     }
 
-    fn accumulate_execute_units_and_time(execute_timings: &ExecuteTimings) -> (u64, u64) {
-        execute_timings.details.per_program_timings.values().fold(
-            (0, 0),
-            |(units, times), program_timings| {
-                (
-                    units.saturating_add(program_timings.accumulated_units),
-                    times.saturating_add(program_timings.accumulated_us),
-                )
-            },
-        )
-    }
-
     /// This function filters pending packets that are still valid
     /// # Arguments
     /// * `transactions` - a batch of transactions deserialized from packets
@@ -749,7 +754,7 @@ mod tests {
         },
         solana_perf::packet::Packet,
         solana_poh::poh_recorder::{PohRecorder, WorkingBankEntry},
-        solana_program_runtime::timings::ProgramTiming,
+        solana_program_runtime::timings::{ExecuteTimings, ProgramTiming},
         solana_rpc::transaction_status_service::TransactionStatusService,
         solana_runtime::{cost_model::CostModel, prioritization_fee_cache::PrioritizationFeeCache},
         solana_sdk::{
@@ -808,7 +813,14 @@ mod tests {
             replay_vote_sender,
             Arc::new(PrioritizationFeeCache::new(0u64)),
         );
-        let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+        let consumer = Consumer::new(
+            committer,
+            recorder,
+            QosService::new(1),
+            None,
+            HashSet::default(),
+            BundleAccountLocker::default(),
+        );
         let process_transactions_summary =
             consumer.process_transactions(&bank, &Instant::now(), &transactions);
 
@@ -964,7 +976,14 @@ mod tests {
                 replay_vote_sender,
                 Arc::new(PrioritizationFeeCache::new(0u64)),
             );
-            let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+            let consumer = Consumer::new(
+                committer,
+                recorder,
+                QosService::new(1),
+                None,
+                HashSet::default(),
+                BundleAccountLocker::default(),
+            );
 
             let process_transactions_batch_output =
                 consumer.process_and_record_transactions(&bank, &transactions, 0);
@@ -989,7 +1008,13 @@ mod tests {
 
             let mut done = false;
             // read entries until I find mine, might be ticks...
-            while let Ok((_bank, (entry, _tick_height))) = entry_receiver.recv() {
+            while let Ok(WorkingBankEntry {
+                bank,
+                entries_ticks,
+            }) = entry_receiver.recv()
+            {
+                assert!(entries_ticks.len() == 1);
+                let entry = &entries_ticks.get(0).unwrap().0;
                 if !entry.is_tick() {
                     trace!("got entry");
                     assert_eq!(entry.transactions.len(), transactions.len());
@@ -1091,7 +1116,14 @@ mod tests {
                 replay_vote_sender,
                 Arc::new(PrioritizationFeeCache::new(0u64)),
             );
-            let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+            let consumer = Consumer::new(
+                committer,
+                recorder,
+                QosService::new(1),
+                None,
+                HashSet::default(),
+                BundleAccountLocker::default(),
+            );
 
             let process_transactions_batch_output =
                 consumer.process_and_record_transactions(&bank, &transactions, 0);
@@ -1177,7 +1209,14 @@ mod tests {
                 replay_vote_sender,
                 Arc::new(PrioritizationFeeCache::new(0u64)),
             );
-            let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+            let consumer = Consumer::new(
+                committer,
+                recorder,
+                QosService::new(1),
+                None,
+                HashSet::default(),
+                BundleAccountLocker::default(),
+            );
 
             let get_block_cost = || bank.read_cost_tracker().unwrap().block_cost();
             let get_tx_count = || bank.read_cost_tracker().unwrap().transaction_count();
@@ -1327,7 +1366,14 @@ mod tests {
                 replay_vote_sender,
                 Arc::new(PrioritizationFeeCache::new(0u64)),
             );
-            let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+            let consumer = Consumer::new(
+                committer,
+                recorder,
+                QosService::new(1),
+                None,
+                HashSet::default(),
+                BundleAccountLocker::default(),
+            );
 
             let process_transactions_batch_output =
                 consumer.process_and_record_transactions(&bank, &transactions, 0);
@@ -1524,7 +1570,14 @@ mod tests {
                 replay_vote_sender,
                 Arc::new(PrioritizationFeeCache::new(0u64)),
             );
-            let consumer = Consumer::new(committer, recorder.clone(), QosService::new(1), None);
+            let consumer = Consumer::new(
+                committer,
+                recorder.clone(),
+                QosService::new(1),
+                None,
+                HashSet::default(),
+                BundleAccountLocker::default(),
+            );
 
             let process_transactions_summary =
                 consumer.process_transactions(&bank, &Instant::now(), &transactions);
@@ -1649,7 +1702,14 @@ mod tests {
                 replay_vote_sender,
                 Arc::new(PrioritizationFeeCache::new(0u64)),
             );
-            let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+            let consumer = Consumer::new(
+                committer,
+                recorder,
+                QosService::new(1),
+                None,
+                HashSet::default(),
+                BundleAccountLocker::default(),
+            );
 
             let _ = consumer.process_and_record_transactions(&bank, &transactions, 0);
 
@@ -1787,7 +1847,14 @@ mod tests {
                 replay_vote_sender,
                 Arc::new(PrioritizationFeeCache::new(0u64)),
             );
-            let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+            let consumer = Consumer::new(
+                committer,
+                recorder,
+                QosService::new(1),
+                None,
+                HashSet::default(),
+                BundleAccountLocker::default(),
+            );
 
             let _ = consumer.process_and_record_transactions(&bank, &[sanitized_tx.clone()], 0);
 
@@ -1847,7 +1914,14 @@ mod tests {
                 replay_vote_sender,
                 Arc::new(PrioritizationFeeCache::new(0u64)),
             );
-            let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+            let consumer = Consumer::new(
+                committer,
+                recorder,
+                QosService::new(1),
+                None,
+                HashSet::default(),
+                BundleAccountLocker::default(),
+            );
 
             // When the working bank in poh_recorder is None, no packets should be processed (consume will not be called)
             assert!(!poh_recorder.read().unwrap().has_bank());
@@ -1925,7 +1999,14 @@ mod tests {
                 replay_vote_sender,
                 Arc::new(PrioritizationFeeCache::new(0u64)),
             );
-            let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+            let consumer = Consumer::new(
+                committer,
+                recorder,
+                QosService::new(1),
+                None,
+                HashSet::default(),
+                BundleAccountLocker::default(),
+            );
 
             // When the working bank in poh_recorder is None, no packets should be processed
             assert!(!poh_recorder.read().unwrap().has_bank());
@@ -1977,7 +2058,14 @@ mod tests {
                 replay_vote_sender,
                 Arc::new(PrioritizationFeeCache::new(0u64)),
             );
-            let consumer = Consumer::new(committer, recorder, QosService::new(1), None);
+            let consumer = Consumer::new(
+                committer,
+                recorder,
+                QosService::new(1),
+                None,
+                HashSet::default(),
+                BundleAccountLocker::default(),
+            );
 
             // When the working bank in poh_recorder is None, no packets should be processed (consume will not be called)
             assert!(!poh_recorder.read().unwrap().has_bank());
@@ -2065,7 +2153,7 @@ mod tests {
             expected_units += n * 1000;
         }
 
-        let (units, us) = Consumer::accumulate_execute_units_and_time(&execute_timings);
+        let (units, us) = execute_timings.accumulate_execute_units_and_time();
 
         assert_eq!(expected_units, units);
         assert_eq!(expected_us, us);
diff --git a/core/src/banking_trace.rs b/core/src/banking_trace.rs
index b245e1fb59..fd6c7c2d90 100644
--- a/core/src/banking_trace.rs
+++ b/core/src/banking_trace.rs
@@ -318,6 +318,7 @@ impl BankingTracer {
     }
 }
 
+#[derive(Clone)]
 pub struct TracedSender {
     label: ChannelLabel,
     sender: Sender<BankingPacketBatch>,
diff --git a/core/src/broadcast_stage.rs b/core/src/broadcast_stage.rs
index 3b6e4967c3..1f5b652383 100644
--- a/core/src/broadcast_stage.rs
+++ b/core/src/broadcast_stage.rs
@@ -36,7 +36,7 @@ use {
     std::{
         collections::{HashMap, HashSet},
         iter::repeat_with,
-        net::UdpSocket,
+        net::{SocketAddr, UdpSocket},
         sync::{
             atomic::{AtomicBool, Ordering},
             Arc, Mutex, RwLock,
@@ -87,6 +87,7 @@ impl BroadcastStageType {
         blockstore: Arc<Blockstore>,
         bank_forks: Arc<RwLock<BankForks>>,
         shred_version: u16,
+        shred_receiver_address: Arc<RwLock<Option<SocketAddr>>>,
     ) -> BroadcastStage {
         match self {
             BroadcastStageType::Standard => BroadcastStage::new(
@@ -98,6 +99,7 @@ impl BroadcastStageType {
                 blockstore,
                 bank_forks,
                 StandardBroadcastRun::new(shred_version),
+                shred_receiver_address,
             ),
 
             BroadcastStageType::FailEntryVerification => BroadcastStage::new(
@@ -109,6 +111,7 @@ impl BroadcastStageType {
                 blockstore,
                 bank_forks,
                 FailEntryVerificationBroadcastRun::new(shred_version),
+                Arc::new(RwLock::new(None)),
             ),
 
             BroadcastStageType::BroadcastFakeShreds => BroadcastStage::new(
@@ -120,6 +123,7 @@ impl BroadcastStageType {
                 blockstore,
                 bank_forks,
                 BroadcastFakeShredsRun::new(0, shred_version),
+                Arc::new(RwLock::new(None)),
             ),
 
             BroadcastStageType::BroadcastDuplicates(config) => BroadcastStage::new(
@@ -131,6 +135,7 @@ impl BroadcastStageType {
                 blockstore,
                 bank_forks,
                 BroadcastDuplicatesRun::new(shred_version, config.clone()),
+                Arc::new(RwLock::new(None)),
             ),
         }
     }
@@ -151,6 +156,7 @@ trait BroadcastRun {
         cluster_info: &ClusterInfo,
         sock: &UdpSocket,
         bank_forks: &RwLock<BankForks>,
+        shred_receiver_address: &Arc<RwLock<Option<SocketAddr>>>,
     ) -> Result<()>;
     fn record(&mut self, receiver: &Mutex<RecordReceiver>, blockstore: &Blockstore) -> Result<()>;
 }
@@ -245,6 +251,7 @@ impl BroadcastStage {
         blockstore: Arc<Blockstore>,
         bank_forks: Arc<RwLock<BankForks>>,
         broadcast_stage_run: impl BroadcastRun + Send + 'static + Clone,
+        shred_receiver_address: Arc<RwLock<Option<SocketAddr>>>,
     ) -> Self {
         let (socket_sender, socket_receiver) = unbounded();
         let (blockstore_sender, blockstore_receiver) = unbounded();
@@ -276,8 +283,16 @@ impl BroadcastStage {
             let mut bs_transmit = broadcast_stage_run.clone();
             let cluster_info = cluster_info.clone();
             let bank_forks = bank_forks.clone();
+            let shred_receiver_address = shred_receiver_address.clone();
+
             let run_transmit = move || loop {
-                let res = bs_transmit.transmit(&socket_receiver, &cluster_info, &sock, &bank_forks);
+                let res = bs_transmit.transmit(
+                    &socket_receiver,
+                    &cluster_info,
+                    &sock,
+                    &bank_forks,
+                    &shred_receiver_address,
+                );
                 let res = Self::handle_error(res, "solana-broadcaster-transmit");
                 if let Some(res) = res {
                     return res;
@@ -397,6 +412,7 @@ pub fn broadcast_shreds(
     cluster_info: &ClusterInfo,
     bank_forks: &RwLock<BankForks>,
     socket_addr_space: &SocketAddrSpace,
+    shred_receiver_address: &Option<SocketAddr>,
 ) -> Result<()> {
     let mut result = Ok(());
     let mut shred_select = Measure::start("shred_select");
@@ -412,13 +428,22 @@ pub fn broadcast_shreds(
             let cluster_nodes =
                 cluster_nodes_cache.get(slot, &root_bank, &working_bank, cluster_info);
             update_peer_stats(&cluster_nodes, last_datapoint_submit);
-            shreds.filter_map(move |shred| {
-                cluster_nodes
-                    .get_broadcast_peer(&shred.id())?
-                    .tvu(Protocol::UDP)
-                    .ok()
-                    .filter(|addr| socket_addr_space.check(addr))
-                    .map(|addr| (shred.payload(), addr))
+            shreds.flat_map(move |shred| {
+                let mut addrs = Vec::with_capacity(2);
+                if let Some(shred_receiver_address) = shred_receiver_address {
+                    addrs.push(*shred_receiver_address);
+                }
+
+                if let Some(peer) = cluster_nodes.get_broadcast_peer(&shred.id()) {
+                    if let Ok(tvu) = peer.tvu(Protocol::UDP) {
+                        addrs.push(tvu);
+                    }
+                }
+
+                addrs
+                    .into_iter()
+                    .filter(|a| socket_addr_space.check(a))
+                    .map(move |addr| (shred.payload(), addr))
             })
         })
         .collect();
@@ -620,6 +645,7 @@ pub mod test {
             blockstore.clone(),
             bank_forks,
             StandardBroadcastRun::new(0),
+            Arc::new(RwLock::new(None)),
         );
 
         MockBroadcastStage {
@@ -659,7 +685,10 @@ pub mod test {
                 let ticks = create_ticks(max_tick_height - start_tick_height, 0, Hash::default());
                 for (i, tick) in ticks.into_iter().enumerate() {
                     entry_sender
-                        .send((bank.clone(), (tick, i as u64 + 1)))
+                        .send(WorkingBankEntry {
+                            bank: bank.clone(),
+                            entries_ticks: vec![(tick, i as u64 + 1)],
+                        })
                         .expect("Expect successful send to broadcast service");
                 }
             }
diff --git a/core/src/broadcast_stage/broadcast_duplicates_run.rs b/core/src/broadcast_stage/broadcast_duplicates_run.rs
index 08eec89838..050e87aa66 100644
--- a/core/src/broadcast_stage/broadcast_duplicates_run.rs
+++ b/core/src/broadcast_stage/broadcast_duplicates_run.rs
@@ -265,6 +265,7 @@ impl BroadcastRun for BroadcastDuplicatesRun {
         cluster_info: &ClusterInfo,
         sock: &UdpSocket,
         bank_forks: &RwLock<BankForks>,
+        _shred_receiver_addr: &Arc<RwLock<Option<SocketAddr>>>,
     ) -> Result<()> {
         let (shreds, _) = receiver.lock().unwrap().recv()?;
         if shreds.is_empty() {
diff --git a/core/src/broadcast_stage/broadcast_fake_shreds_run.rs b/core/src/broadcast_stage/broadcast_fake_shreds_run.rs
index 01650aad32..6b23429029 100644
--- a/core/src/broadcast_stage/broadcast_fake_shreds_run.rs
+++ b/core/src/broadcast_stage/broadcast_fake_shreds_run.rs
@@ -132,6 +132,7 @@ impl BroadcastRun for BroadcastFakeShredsRun {
         cluster_info: &ClusterInfo,
         sock: &UdpSocket,
         _bank_forks: &RwLock<BankForks>,
+        _shred_receiver_addr: &Arc<RwLock<Option<SocketAddr>>>,
     ) -> Result<()> {
         for (data_shreds, batch_info) in receiver.lock().unwrap().iter() {
             let fake = batch_info.is_some();
diff --git a/core/src/broadcast_stage/broadcast_utils.rs b/core/src/broadcast_stage/broadcast_utils.rs
index f9485d59a9..6150bf4fec 100644
--- a/core/src/broadcast_stage/broadcast_utils.rs
+++ b/core/src/broadcast_stage/broadcast_utils.rs
@@ -36,13 +36,22 @@ pub(super) fn recv_slot_entries(receiver: &Receiver<WorkingBankEntry>) -> Result
         32 * ShredData::capacity(/*merkle_proof_size*/ None).unwrap() as u64;
     let timer = Duration::new(1, 0);
     let recv_start = Instant::now();
-    let (mut bank, (entry, mut last_tick_height)) = receiver.recv_timeout(timer)?;
-    let mut entries = vec![entry];
+
+    let WorkingBankEntry {
+        mut bank,
+        entries_ticks,
+    } = receiver.recv_timeout(timer)?;
+    let mut last_tick_height = entries_ticks.iter().last().unwrap().1;
+    let mut entries: Vec<Entry> = entries_ticks.into_iter().map(|(e, _)| e).collect();
+
     assert!(last_tick_height <= bank.max_tick_height());
 
     // Drain channel
     while last_tick_height != bank.max_tick_height() {
-        let (try_bank, (entry, tick_height)) = match receiver.try_recv() {
+        let WorkingBankEntry {
+            bank: try_bank,
+            entries_ticks: new_entries_ticks,
+        } = match receiver.try_recv() {
             Ok(working_bank_entry) => working_bank_entry,
             Err(_) => break,
         };
@@ -53,8 +62,8 @@ pub(super) fn recv_slot_entries(receiver: &Receiver<WorkingBankEntry>) -> Result
             entries.clear();
             bank = try_bank;
         }
-        last_tick_height = tick_height;
-        entries.push(entry);
+        last_tick_height = new_entries_ticks.iter().last().unwrap().1;
+        entries.extend(new_entries_ticks.into_iter().map(|(entry, _)| entry));
         assert!(last_tick_height <= bank.max_tick_height());
     }
 
@@ -65,11 +74,13 @@ pub(super) fn recv_slot_entries(receiver: &Receiver<WorkingBankEntry>) -> Result
     while last_tick_height != bank.max_tick_height()
         && serialized_batch_byte_count < target_serialized_batch_byte_count
     {
-        let (try_bank, (entry, tick_height)) =
-            match receiver.recv_deadline(coalesce_start + ENTRY_COALESCE_DURATION) {
-                Ok(working_bank_entry) => working_bank_entry,
-                Err(_) => break,
-            };
+        let WorkingBankEntry {
+            bank: try_bank,
+            entries_ticks: new_entries_ticks,
+        } = match receiver.recv_deadline(coalesce_start + ENTRY_COALESCE_DURATION) {
+            Ok(working_bank_entry) => working_bank_entry,
+            Err(_) => break,
+        };
         // If the bank changed, that implies the previous slot was interrupted and we do not have to
         // broadcast its entries.
         if try_bank.slot() != bank.slot() {
@@ -79,10 +90,12 @@ pub(super) fn recv_slot_entries(receiver: &Receiver<WorkingBankEntry>) -> Result
             bank = try_bank;
             coalesce_start = Instant::now();
         }
-        last_tick_height = tick_height;
-        let entry_bytes = serialized_size(&entry)?;
-        serialized_batch_byte_count += entry_bytes;
-        entries.push(entry);
+        last_tick_height = new_entries_ticks.iter().last().unwrap().1;
+
+        for (entry, _) in &new_entries_ticks {
+            serialized_batch_byte_count += serialized_size(entry)?;
+        }
+        entries.extend(new_entries_ticks.into_iter().map(|(entry, _)| entry));
         assert!(last_tick_height <= bank.max_tick_height());
     }
     let time_coalesced = coalesce_start.elapsed();
@@ -139,7 +152,11 @@ mod tests {
             .map(|i| {
                 let entry = Entry::new(&last_hash, 1, vec![tx.clone()]);
                 last_hash = entry.hash;
-                s.send((bank1.clone(), (entry.clone(), i))).unwrap();
+                s.send(WorkingBankEntry {
+                    bank: bank1.clone(),
+                    entries_ticks: vec![(entry.clone(), i)],
+                })
+                .unwrap();
                 entry
             })
             .collect();
@@ -173,11 +190,18 @@ mod tests {
                 last_hash = entry.hash;
                 // Interrupt slot 1 right before the last tick
                 if tick_height == expected_last_height {
-                    s.send((bank2.clone(), (entry.clone(), tick_height)))
-                        .unwrap();
+                    s.send(WorkingBankEntry {
+                        bank: bank2.clone(),
+                        entries_ticks: vec![(entry.clone(), tick_height)],
+                    })
+                    .unwrap();
                     Some(entry)
                 } else {
-                    s.send((bank1.clone(), (entry, tick_height))).unwrap();
+                    s.send(WorkingBankEntry {
+                        bank: bank1.clone(),
+                        entries_ticks: vec![(entry, tick_height)],
+                    })
+                    .unwrap();
                     None
                 }
             })
diff --git a/core/src/broadcast_stage/fail_entry_verification_broadcast_run.rs b/core/src/broadcast_stage/fail_entry_verification_broadcast_run.rs
index e7b899ab0f..61ea979402 100644
--- a/core/src/broadcast_stage/fail_entry_verification_broadcast_run.rs
+++ b/core/src/broadcast_stage/fail_entry_verification_broadcast_run.rs
@@ -3,7 +3,7 @@ use {
     crate::cluster_nodes::ClusterNodesCache,
     solana_ledger::shred::{ProcessShredsStats, ReedSolomonCache, Shredder},
     solana_sdk::{hash::Hash, signature::Keypair},
-    std::{thread::sleep, time::Duration},
+    std::{net::SocketAddr, thread::sleep, time::Duration},
 };
 
 pub const NUM_BAD_SLOTS: u64 = 10;
@@ -162,6 +162,7 @@ impl BroadcastRun for FailEntryVerificationBroadcastRun {
         cluster_info: &ClusterInfo,
         sock: &UdpSocket,
         bank_forks: &RwLock<BankForks>,
+        shred_receiver_address: &Arc<RwLock<Option<SocketAddr>>>,
     ) -> Result<()> {
         let (shreds, _) = receiver.lock().unwrap().recv()?;
         broadcast_shreds(
@@ -173,6 +174,7 @@ impl BroadcastRun for FailEntryVerificationBroadcastRun {
             cluster_info,
             bank_forks,
             cluster_info.socket_addr_space(),
+            &shred_receiver_address.read().unwrap(),
         )
     }
     fn record(&mut self, receiver: &Mutex<RecordReceiver>, blockstore: &Blockstore) -> Result<()> {
diff --git a/core/src/broadcast_stage/standard_broadcast_run.rs b/core/src/broadcast_stage/standard_broadcast_run.rs
index c22224b1fa..196883d453 100644
--- a/core/src/broadcast_stage/standard_broadcast_run.rs
+++ b/core/src/broadcast_stage/standard_broadcast_run.rs
@@ -17,7 +17,7 @@ use {
         signature::Keypair,
         timing::{duration_as_us, AtomicInterval},
     },
-    std::{sync::RwLock, time::Duration},
+    std::{net::SocketAddr, sync::RwLock, time::Duration},
 };
 
 #[derive(Clone)]
@@ -191,10 +191,22 @@ impl StandardBroadcastRun {
         let brecv = Arc::new(Mutex::new(brecv));
 
         //data
-        let _ = self.transmit(&srecv, cluster_info, sock, bank_forks);
+        let _ = self.transmit(
+            &srecv,
+            cluster_info,
+            sock,
+            bank_forks,
+            &Arc::new(RwLock::new(None)),
+        );
         let _ = self.record(&brecv, blockstore);
         //coding
-        let _ = self.transmit(&srecv, cluster_info, sock, bank_forks);
+        let _ = self.transmit(
+            &srecv,
+            cluster_info,
+            sock,
+            bank_forks,
+            &Arc::new(RwLock::new(None)),
+        );
         let _ = self.record(&brecv, blockstore);
         Ok(())
     }
@@ -387,6 +399,7 @@ impl StandardBroadcastRun {
         shreds: Arc<Vec<Shred>>,
         broadcast_shred_batch_info: Option<BroadcastShredBatchInfo>,
         bank_forks: &RwLock<BankForks>,
+        shred_receiver_addr: &Option<SocketAddr>,
     ) -> Result<()> {
         trace!("Broadcasting {:?} shreds", shreds.len());
         let mut transmit_stats = TransmitShredsStats::default();
@@ -402,6 +415,7 @@ impl StandardBroadcastRun {
             cluster_info,
             bank_forks,
             cluster_info.socket_addr_space(),
+            shred_receiver_addr,
         )?;
         transmit_time.stop();
 
@@ -471,9 +485,17 @@ impl BroadcastRun for StandardBroadcastRun {
         cluster_info: &ClusterInfo,
         sock: &UdpSocket,
         bank_forks: &RwLock<BankForks>,
+        shred_receiver_address: &Arc<RwLock<Option<SocketAddr>>>,
     ) -> Result<()> {
         let (shreds, batch_info) = receiver.lock().unwrap().recv()?;
-        self.broadcast(sock, cluster_info, shreds, batch_info, bank_forks)
+        self.broadcast(
+            sock,
+            cluster_info,
+            shreds,
+            batch_info,
+            bank_forks,
+            &shred_receiver_address.read().unwrap(),
+        )
     }
     fn record(&mut self, receiver: &Mutex<RecordReceiver>, blockstore: &Blockstore) -> Result<()> {
         let (shreds, slot_start_ts) = receiver.lock().unwrap().recv()?;
diff --git a/core/src/bundle_stage.rs b/core/src/bundle_stage.rs
new file mode 100644
index 0000000000..feff5512cc
--- /dev/null
+++ b/core/src/bundle_stage.rs
@@ -0,0 +1,435 @@
+//! The `bundle_stage` processes bundles, which are list of transactions to be executed
+//! sequentially and atomically.
+use {
+    crate::{
+        banking_stage::decision_maker::{BufferedPacketsDecision, DecisionMaker},
+        bundle_stage::{
+            bundle_account_locker::BundleAccountLocker, bundle_consumer::BundleConsumer,
+            bundle_packet_receiver::BundleReceiver,
+            bundle_reserved_space_manager::BundleReservedSpaceManager,
+            bundle_stage_leader_metrics::BundleStageLeaderMetrics, committer::Committer,
+        },
+        packet_bundle::PacketBundle,
+        proxy::block_engine_stage::BlockBuilderFeeInfo,
+        qos_service::QosService,
+        tip_manager::TipManager,
+        unprocessed_transaction_storage::UnprocessedTransactionStorage,
+    },
+    crossbeam_channel::{Receiver, RecvTimeoutError},
+    solana_gossip::cluster_info::ClusterInfo,
+    solana_ledger::blockstore_processor::TransactionStatusSender,
+    solana_measure::measure,
+    solana_poh::poh_recorder::PohRecorder,
+    solana_runtime::{
+        bank_forks::BankForks, block_cost_limits::MAX_BLOCK_UNITS,
+        prioritization_fee_cache::PrioritizationFeeCache, vote_sender_types::ReplayVoteSender,
+    },
+    solana_sdk::timing::AtomicInterval,
+    std::{
+        collections::VecDeque,
+        sync::{
+            atomic::{AtomicBool, AtomicU64, Ordering},
+            Arc, Mutex, RwLock,
+        },
+        thread::{self, Builder, JoinHandle},
+        time::{Duration, Instant},
+    },
+};
+
+pub mod bundle_account_locker;
+mod bundle_consumer;
+mod bundle_packet_deserializer;
+mod bundle_packet_receiver;
+mod bundle_reserved_space_manager;
+pub(crate) mod bundle_stage_leader_metrics;
+mod committer;
+
+const MAX_BUNDLE_RETRY_DURATION: Duration = Duration::from_millis(10);
+const SLOT_BOUNDARY_CHECK_PERIOD: Duration = Duration::from_millis(10);
+
+// Stats emitted periodically
+#[derive(Default)]
+pub struct BundleStageLoopMetrics {
+    last_report: AtomicInterval,
+    id: u32,
+
+    // total received
+    num_bundles_received: AtomicU64,
+    num_packets_received: AtomicU64,
+
+    // newly buffered
+    newly_buffered_bundles_count: AtomicU64,
+
+    // currently buffered
+    current_buffered_bundles_count: AtomicU64,
+    current_buffered_packets_count: AtomicU64,
+
+    // buffered due to cost model
+    cost_model_buffered_bundles_count: AtomicU64,
+    cost_model_buffered_packets_count: AtomicU64,
+
+    // number of bundles dropped during insertion
+    num_bundles_dropped: AtomicU64,
+
+    // timings
+    receive_and_buffer_bundles_elapsed_us: AtomicU64,
+    process_buffered_bundles_elapsed_us: AtomicU64,
+}
+
+impl BundleStageLoopMetrics {
+    fn new(id: u32) -> Self {
+        BundleStageLoopMetrics {
+            id,
+            ..BundleStageLoopMetrics::default()
+        }
+    }
+
+    pub fn increment_num_bundles_received(&mut self, count: u64) {
+        self.num_bundles_received
+            .fetch_add(count, Ordering::Relaxed);
+    }
+
+    pub fn increment_num_packets_received(&mut self, count: u64) {
+        self.num_packets_received
+            .fetch_add(count, Ordering::Relaxed);
+    }
+
+    pub fn increment_newly_buffered_bundles_count(&mut self, count: u64) {
+        self.newly_buffered_bundles_count
+            .fetch_add(count, Ordering::Relaxed);
+    }
+
+    pub fn increment_current_buffered_bundles_count(&mut self, count: u64) {
+        self.current_buffered_bundles_count
+            .fetch_add(count, Ordering::Relaxed);
+    }
+
+    pub fn increment_current_buffered_packets_count(&mut self, count: u64) {
+        self.current_buffered_packets_count
+            .fetch_add(count, Ordering::Relaxed);
+    }
+
+    pub fn increment_cost_model_buffered_bundles_count(&mut self, count: u64) {
+        self.cost_model_buffered_bundles_count
+            .fetch_add(count, Ordering::Relaxed);
+    }
+
+    pub fn increment_cost_model_buffered_packets_count(&mut self, count: u64) {
+        self.cost_model_buffered_packets_count
+            .fetch_add(count, Ordering::Relaxed);
+    }
+
+    pub fn increment_num_bundles_dropped(&mut self, count: u64) {
+        self.num_bundles_dropped.fetch_add(count, Ordering::Relaxed);
+    }
+
+    pub fn increment_receive_and_buffer_bundles_elapsed_us(&mut self, count: u64) {
+        self.receive_and_buffer_bundles_elapsed_us
+            .fetch_add(count, Ordering::Relaxed);
+    }
+
+    pub fn increment_process_buffered_bundles_elapsed_us(&mut self, count: u64) {
+        self.process_buffered_bundles_elapsed_us
+            .fetch_add(count, Ordering::Relaxed);
+    }
+}
+
+impl BundleStageLoopMetrics {
+    fn maybe_report(&mut self, report_interval_ms: u64) {
+        if self.last_report.should_update(report_interval_ms) {
+            datapoint_info!(
+                "bundle_stage-loop_stats",
+                ("id", self.id, i64),
+                (
+                    "num_bundles_received",
+                    self.num_bundles_received.swap(0, Ordering::Acquire) as i64,
+                    i64
+                ),
+                (
+                    "num_packets_received",
+                    self.num_packets_received.swap(0, Ordering::Acquire) as i64,
+                    i64
+                ),
+                (
+                    "newly_buffered_bundles_count",
+                    self.newly_buffered_bundles_count.swap(0, Ordering::Acquire) as i64,
+                    i64
+                ),
+                (
+                    "current_buffered_bundles_count",
+                    self.current_buffered_bundles_count
+                        .swap(0, Ordering::Acquire) as i64,
+                    i64
+                ),
+                (
+                    "current_buffered_packets_count",
+                    self.current_buffered_packets_count
+                        .swap(0, Ordering::Acquire) as i64,
+                    i64
+                ),
+                (
+                    "num_bundles_dropped",
+                    self.num_bundles_dropped.swap(0, Ordering::Acquire) as i64,
+                    i64
+                ),
+                (
+                    "receive_and_buffer_bundles_elapsed_us",
+                    self.receive_and_buffer_bundles_elapsed_us
+                        .swap(0, Ordering::Acquire) as i64,
+                    i64
+                ),
+                (
+                    "process_buffered_bundles_elapsed_us",
+                    self.process_buffered_bundles_elapsed_us
+                        .swap(0, Ordering::Acquire) as i64,
+                    i64
+                ),
+            );
+        }
+    }
+}
+
+pub struct BundleStage {
+    bundle_thread: JoinHandle<()>,
+}
+
+impl BundleStage {
+    #[allow(clippy::new_ret_no_self)]
+    #[allow(clippy::too_many_arguments)]
+    pub fn new(
+        cluster_info: &Arc<ClusterInfo>,
+        poh_recorder: &Arc<RwLock<PohRecorder>>,
+        bundle_receiver: Receiver<Vec<PacketBundle>>,
+        transaction_status_sender: Option<TransactionStatusSender>,
+        replay_vote_sender: ReplayVoteSender,
+        log_messages_bytes_limit: Option<usize>,
+        exit: Arc<AtomicBool>,
+        tip_manager: TipManager,
+        bundle_account_locker: BundleAccountLocker,
+        block_builder_fee_info: &Arc<Mutex<BlockBuilderFeeInfo>>,
+        preallocated_bundle_cost: u64,
+        bank_forks: Arc<RwLock<BankForks>>,
+        prioritization_fee_cache: &Arc<PrioritizationFeeCache>,
+    ) -> Self {
+        Self::start_bundle_thread(
+            cluster_info,
+            poh_recorder,
+            bundle_receiver,
+            transaction_status_sender,
+            replay_vote_sender,
+            log_messages_bytes_limit,
+            exit,
+            tip_manager,
+            bundle_account_locker,
+            MAX_BUNDLE_RETRY_DURATION,
+            block_builder_fee_info,
+            preallocated_bundle_cost,
+            bank_forks,
+            prioritization_fee_cache,
+        )
+    }
+
+    pub fn join(self) -> thread::Result<()> {
+        self.bundle_thread.join()
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    fn start_bundle_thread(
+        cluster_info: &Arc<ClusterInfo>,
+        poh_recorder: &Arc<RwLock<PohRecorder>>,
+        bundle_receiver: Receiver<Vec<PacketBundle>>,
+        transaction_status_sender: Option<TransactionStatusSender>,
+        replay_vote_sender: ReplayVoteSender,
+        log_message_bytes_limit: Option<usize>,
+        exit: Arc<AtomicBool>,
+        tip_manager: TipManager,
+        bundle_account_locker: BundleAccountLocker,
+        max_bundle_retry_duration: Duration,
+        block_builder_fee_info: &Arc<Mutex<BlockBuilderFeeInfo>>,
+        preallocated_bundle_cost: u64,
+        bank_forks: Arc<RwLock<BankForks>>,
+        prioritization_fee_cache: &Arc<PrioritizationFeeCache>,
+    ) -> Self {
+        const BUNDLE_STAGE_ID: u32 = 10_000;
+        let poh_recorder = poh_recorder.clone();
+        let cluster_info = cluster_info.clone();
+
+        let mut bundle_receiver =
+            BundleReceiver::new(BUNDLE_STAGE_ID, bundle_receiver, bank_forks, Some(5));
+
+        let committer = Committer::new(
+            transaction_status_sender,
+            replay_vote_sender,
+            prioritization_fee_cache.clone(),
+        );
+        let decision_maker = DecisionMaker::new(cluster_info.id(), poh_recorder.clone());
+
+        let unprocessed_bundle_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(1_000),
+            VecDeque::with_capacity(1_000),
+        );
+
+        let reserved_ticks = poh_recorder
+            .read()
+            .unwrap()
+            .ticks_per_slot()
+            .saturating_mul(8)
+            .saturating_div(10);
+
+        // The first 80% of the block, based on poh ticks, has `preallocated_bundle_cost` less compute units.
+        // The last 20% has has full compute so blockspace is maximized if BundleStage is idle.
+        let reserved_space = BundleReservedSpaceManager::new(
+            MAX_BLOCK_UNITS,
+            preallocated_bundle_cost,
+            reserved_ticks,
+        );
+
+        let consumer = BundleConsumer::new(
+            committer,
+            poh_recorder.read().unwrap().new_recorder(),
+            QosService::new(BUNDLE_STAGE_ID),
+            log_message_bytes_limit,
+            tip_manager,
+            bundle_account_locker,
+            block_builder_fee_info.clone(),
+            max_bundle_retry_duration,
+            cluster_info,
+            reserved_space,
+        );
+
+        let bundle_thread = Builder::new()
+            .name("solBundleStgTx".to_string())
+            .spawn(move || {
+                Self::process_loop(
+                    &mut bundle_receiver,
+                    decision_maker,
+                    consumer,
+                    BUNDLE_STAGE_ID,
+                    unprocessed_bundle_storage,
+                    exit,
+                );
+            })
+            .unwrap();
+
+        Self { bundle_thread }
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    fn process_loop(
+        bundle_receiver: &mut BundleReceiver,
+        decision_maker: DecisionMaker,
+        mut consumer: BundleConsumer,
+        id: u32,
+        mut unprocessed_bundle_storage: UnprocessedTransactionStorage,
+        exit: Arc<AtomicBool>,
+    ) {
+        let mut last_metrics_update = Instant::now();
+
+        let mut bundle_stage_metrics = BundleStageLoopMetrics::new(id);
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(id);
+
+        while !exit.load(Ordering::Relaxed) {
+            if !unprocessed_bundle_storage.is_empty()
+                || last_metrics_update.elapsed() >= SLOT_BOUNDARY_CHECK_PERIOD
+            {
+                let (_, process_buffered_packets_time) = measure!(
+                    Self::process_buffered_bundles(
+                        &decision_maker,
+                        &mut consumer,
+                        &mut unprocessed_bundle_storage,
+                        &mut bundle_stage_leader_metrics,
+                    ),
+                    "process_buffered_packets",
+                );
+                bundle_stage_leader_metrics
+                    .leader_slot_metrics_tracker()
+                    .increment_process_buffered_packets_us(process_buffered_packets_time.as_us());
+                last_metrics_update = Instant::now();
+            }
+
+            match bundle_receiver.receive_and_buffer_bundles(
+                &mut unprocessed_bundle_storage,
+                &mut bundle_stage_metrics,
+                &mut bundle_stage_leader_metrics,
+            ) {
+                Ok(_) | Err(RecvTimeoutError::Timeout) => (),
+                Err(RecvTimeoutError::Disconnected) => break,
+            }
+
+            let bundle_storage = unprocessed_bundle_storage.bundle_storage().unwrap();
+            bundle_stage_metrics.increment_current_buffered_bundles_count(
+                bundle_storage.unprocessed_bundles_len() as u64,
+            );
+            bundle_stage_metrics.increment_current_buffered_packets_count(
+                bundle_storage.unprocessed_packets_len() as u64,
+            );
+            bundle_stage_metrics.increment_cost_model_buffered_bundles_count(
+                bundle_storage.cost_model_buffered_bundles_len() as u64,
+            );
+            bundle_stage_metrics.increment_cost_model_buffered_packets_count(
+                bundle_storage.cost_model_buffered_packets_len() as u64,
+            );
+            bundle_stage_metrics.maybe_report(1_000);
+        }
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    fn process_buffered_bundles(
+        decision_maker: &DecisionMaker,
+        consumer: &mut BundleConsumer,
+        unprocessed_bundle_storage: &mut UnprocessedTransactionStorage,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+    ) {
+        let (decision, make_decision_time) =
+            measure!(decision_maker.make_consume_or_forward_decision());
+
+        let (metrics_action, banking_stage_metrics_action) =
+            bundle_stage_leader_metrics.check_leader_slot_boundary(decision.bank_start());
+        bundle_stage_leader_metrics
+            .leader_slot_metrics_tracker()
+            .increment_make_decision_us(make_decision_time.as_us());
+
+        match decision {
+            // BufferedPacketsDecision::Consume means this leader is scheduled to be running at the moment.
+            // Execute, record, and commit as many bundles possible given time, compute, and other constraints.
+            BufferedPacketsDecision::Consume(bank_start) => {
+                // Take metrics action before consume packets (potentially resetting the
+                // slot metrics tracker to the next slot) so that we don't count the
+                // packet processing metrics from the next slot towards the metrics
+                // of the previous slot
+                bundle_stage_leader_metrics
+                    .apply_action(metrics_action, banking_stage_metrics_action);
+
+                let (_, consume_buffered_packets_time) = measure!(
+                    consumer.consume_buffered_bundles(
+                        &bank_start,
+                        unprocessed_bundle_storage,
+                        bundle_stage_leader_metrics,
+                    ),
+                    "consume_buffered_bundles",
+                );
+                bundle_stage_leader_metrics
+                    .leader_slot_metrics_tracker()
+                    .increment_consume_buffered_packets_us(consume_buffered_packets_time.as_us());
+            }
+            // BufferedPacketsDecision::Forward means the leader is slot is far away.
+            // Bundles aren't forwarded because it breaks atomicity guarantees, so just drop them.
+            BufferedPacketsDecision::Forward => {
+                let (_num_bundles_cleared, _num_cost_model_buffered_bundles) =
+                    unprocessed_bundle_storage.bundle_storage().unwrap().reset();
+
+                // TODO (LB): add metrics here for how many bundles were cleared
+
+                bundle_stage_leader_metrics
+                    .apply_action(metrics_action, banking_stage_metrics_action);
+            }
+            // BufferedPacketsDecision::ForwardAndHold | BufferedPacketsDecision::Hold means the validator
+            // is approaching the leader slot, hold bundles. Also, bundles aren't forwarded because it breaks
+            // atomicity guarantees
+            BufferedPacketsDecision::ForwardAndHold | BufferedPacketsDecision::Hold => {
+                bundle_stage_leader_metrics
+                    .apply_action(metrics_action, banking_stage_metrics_action);
+            }
+        }
+    }
+}
diff --git a/core/src/bundle_stage/bundle_account_locker.rs b/core/src/bundle_stage/bundle_account_locker.rs
new file mode 100644
index 0000000000..7382fcb5b0
--- /dev/null
+++ b/core/src/bundle_stage/bundle_account_locker.rs
@@ -0,0 +1,328 @@
+//! Handles pre-locking bundle accounts so that accounts bundles touch can be reserved ahead
+// of time for execution. Also, ensures that ALL accounts mentioned across a bundle are locked
+// to avoid race conditions between BundleStage and BankingStage.
+//
+// For instance, imagine a bundle with three transactions and the set of accounts for each transaction
+// is: {{A, B}, {B, C}, {C, D}}. We need to lock A, B, and C even though only one is executed at a time.
+// Imagine BundleStage is in the middle of processing {C, D} and we didn't have a lock on accounts {A, B, C}.
+// In this situation, there's a chance that BankingStage can process a transaction containing A or B
+// and commit the results before the bundle completes. By the time the bundle commits the new account
+// state for {A, B, C}, A and B would be incorrect and the entries containing the bundle would be
+// replayed improperly and that leader would have produced an invalid block.
+use {
+    solana_runtime::bank::Bank,
+    solana_sdk::{bundle::SanitizedBundle, pubkey::Pubkey, transaction::TransactionAccountLocks},
+    std::{
+        collections::{hash_map::Entry, HashMap, HashSet},
+        sync::{Arc, Mutex, MutexGuard},
+    },
+    thiserror::Error,
+};
+
+#[derive(Clone, Error, Debug)]
+pub enum BundleAccountLockerError {
+    #[error("locking error")]
+    LockingError,
+}
+
+pub type BundleAccountLockerResult<T> = Result<T, BundleAccountLockerError>;
+
+pub struct LockedBundle<'a, 'b> {
+    bundle_account_locker: &'a BundleAccountLocker,
+    sanitized_bundle: &'b SanitizedBundle,
+    bank: Arc<Bank>,
+}
+
+impl<'a, 'b> LockedBundle<'a, 'b> {
+    pub fn new(
+        bundle_account_locker: &'a BundleAccountLocker,
+        sanitized_bundle: &'b SanitizedBundle,
+        bank: &Arc<Bank>,
+    ) -> Self {
+        Self {
+            bundle_account_locker,
+            sanitized_bundle,
+            bank: bank.clone(),
+        }
+    }
+
+    pub fn sanitized_bundle(&self) -> &SanitizedBundle {
+        self.sanitized_bundle
+    }
+}
+
+// Automatically unlock bundle accounts when destructed
+impl<'a, 'b> Drop for LockedBundle<'a, 'b> {
+    fn drop(&mut self) {
+        let _ = self
+            .bundle_account_locker
+            .unlock_bundle_accounts(self.sanitized_bundle, &self.bank);
+    }
+}
+
+#[derive(Default, Clone)]
+pub struct BundleAccountLocks {
+    read_locks: HashMap<Pubkey, u64>,
+    write_locks: HashMap<Pubkey, u64>,
+}
+
+impl BundleAccountLocks {
+    pub fn read_locks(&self) -> HashSet<Pubkey> {
+        self.read_locks.keys().cloned().collect()
+    }
+
+    pub fn write_locks(&self) -> HashSet<Pubkey> {
+        self.write_locks.keys().cloned().collect()
+    }
+
+    pub fn lock_accounts(
+        &mut self,
+        read_locks: HashMap<Pubkey, u64>,
+        write_locks: HashMap<Pubkey, u64>,
+    ) {
+        for (acc, count) in read_locks {
+            *self.read_locks.entry(acc).or_insert(0) += count;
+        }
+        for (acc, count) in write_locks {
+            *self.write_locks.entry(acc).or_insert(0) += count;
+        }
+    }
+
+    pub fn unlock_accounts(
+        &mut self,
+        read_locks: HashMap<Pubkey, u64>,
+        write_locks: HashMap<Pubkey, u64>,
+    ) {
+        for (acc, count) in read_locks {
+            if let Entry::Occupied(mut entry) = self.read_locks.entry(acc) {
+                let val = entry.get_mut();
+                *val = val.saturating_sub(count);
+                if entry.get() == &0 {
+                    let _ = entry.remove();
+                }
+            } else {
+                warn!("error unlocking read-locked account, account: {:?}", acc);
+            }
+        }
+        for (acc, count) in write_locks {
+            if let Entry::Occupied(mut entry) = self.write_locks.entry(acc) {
+                let val = entry.get_mut();
+                *val = val.saturating_sub(count);
+                if entry.get() == &0 {
+                    let _ = entry.remove();
+                }
+            } else {
+                warn!("error unlocking write-locked account, account: {:?}", acc);
+            }
+        }
+    }
+}
+
+#[derive(Clone, Default)]
+pub struct BundleAccountLocker {
+    account_locks: Arc<Mutex<BundleAccountLocks>>,
+}
+
+impl BundleAccountLocker {
+    /// used in BankingStage during TransactionBatch construction to ensure that BankingStage
+    /// doesn't lock anything currently locked in the BundleAccountLocker
+    pub fn read_locks(&self) -> HashSet<Pubkey> {
+        self.account_locks.lock().unwrap().read_locks()
+    }
+
+    /// used in BankingStage during TransactionBatch construction to ensure that BankingStage
+    /// doesn't lock anything currently locked in the BundleAccountLocker
+    pub fn write_locks(&self) -> HashSet<Pubkey> {
+        self.account_locks.lock().unwrap().write_locks()
+    }
+
+    /// used in BankingStage during TransactionBatch construction to ensure that BankingStage
+    /// doesn't lock anything currently locked in the BundleAccountLocker
+    pub fn account_locks(&self) -> MutexGuard<BundleAccountLocks> {
+        self.account_locks.lock().unwrap()
+    }
+
+    /// Prepares a locked bundle and returns a LockedBundle containing locked accounts.
+    /// When a LockedBundle is dropped, the accounts are automatically unlocked
+    pub fn prepare_locked_bundle<'a, 'b>(
+        &'a self,
+        sanitized_bundle: &'b SanitizedBundle,
+        bank: &Arc<Bank>,
+    ) -> BundleAccountLockerResult<LockedBundle<'a, 'b>> {
+        let (read_locks, write_locks) = Self::get_read_write_locks(sanitized_bundle, bank)?;
+
+        self.account_locks
+            .lock()
+            .unwrap()
+            .lock_accounts(read_locks, write_locks);
+        Ok(LockedBundle::new(self, sanitized_bundle, bank))
+    }
+
+    /// Unlocks bundle accounts. Note that LockedBundle::drop will auto-drop the bundle account locks
+    fn unlock_bundle_accounts(
+        &self,
+        sanitized_bundle: &SanitizedBundle,
+        bank: &Bank,
+    ) -> BundleAccountLockerResult<()> {
+        let (read_locks, write_locks) = Self::get_read_write_locks(sanitized_bundle, bank)?;
+
+        self.account_locks
+            .lock()
+            .unwrap()
+            .unlock_accounts(read_locks, write_locks);
+        Ok(())
+    }
+
+    /// Returns the read and write locks for this bundle
+    /// Each lock type contains a HashMap which maps Pubkey to number of locks held
+    fn get_read_write_locks(
+        bundle: &SanitizedBundle,
+        bank: &Bank,
+    ) -> BundleAccountLockerResult<(HashMap<Pubkey, u64>, HashMap<Pubkey, u64>)> {
+        let transaction_locks: Vec<TransactionAccountLocks> = bundle
+            .transactions
+            .iter()
+            .filter_map(|tx| {
+                tx.get_account_locks(bank.get_transaction_account_lock_limit())
+                    .ok()
+            })
+            .collect();
+
+        if transaction_locks.len() != bundle.transactions.len() {
+            return Err(BundleAccountLockerError::LockingError);
+        }
+
+        let bundle_read_locks = transaction_locks
+            .iter()
+            .flat_map(|tx| tx.readonly.iter().map(|a| **a));
+        let bundle_read_locks =
+            bundle_read_locks
+                .into_iter()
+                .fold(HashMap::new(), |mut map, acc| {
+                    *map.entry(acc).or_insert(0) += 1;
+                    map
+                });
+
+        let bundle_write_locks = transaction_locks
+            .iter()
+            .flat_map(|tx| tx.writable.iter().map(|a| **a));
+        let bundle_write_locks =
+            bundle_write_locks
+                .into_iter()
+                .fold(HashMap::new(), |mut map, acc| {
+                    *map.entry(acc).or_insert(0) += 1;
+                    map
+                });
+
+        Ok((bundle_read_locks, bundle_write_locks))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {
+        crate::{
+            bundle_stage::bundle_account_locker::BundleAccountLocker,
+            immutable_deserialized_bundle::ImmutableDeserializedBundle,
+            packet_bundle::PacketBundle,
+        },
+        solana_ledger::genesis_utils::create_genesis_config,
+        solana_perf::packet::PacketBatch,
+        solana_runtime::{
+            bank::Bank, genesis_utils::GenesisConfigInfo,
+            transaction_error_metrics::TransactionErrorMetrics,
+        },
+        solana_sdk::{
+            packet::Packet, signature::Signer, signer::keypair::Keypair, system_program,
+            system_transaction::transfer, transaction::VersionedTransaction,
+        },
+        std::{collections::HashSet, sync::Arc},
+    };
+
+    #[test]
+    fn test_simple_lock_bundles() {
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(2);
+        let bank = Arc::new(Bank::new_no_wallclock_throttle_for_tests(&genesis_config));
+
+        let bundle_account_locker = BundleAccountLocker::default();
+
+        let kp0 = Keypair::new();
+        let kp1 = Keypair::new();
+
+        let tx0 = VersionedTransaction::from(transfer(
+            &mint_keypair,
+            &kp0.pubkey(),
+            1,
+            genesis_config.hash(),
+        ));
+        let tx1 = VersionedTransaction::from(transfer(
+            &mint_keypair,
+            &kp1.pubkey(),
+            1,
+            genesis_config.hash(),
+        ));
+
+        let mut packet_bundle0 = PacketBundle {
+            batch: PacketBatch::new(vec![Packet::from_data(None, &tx0).unwrap()]),
+            bundle_id: tx0.signatures[0].to_string(),
+        };
+        let mut packet_bundle1 = PacketBundle {
+            batch: PacketBatch::new(vec![Packet::from_data(None, &tx1).unwrap()]),
+            bundle_id: tx1.signatures[0].to_string(),
+        };
+
+        let mut transaction_errors = TransactionErrorMetrics::default();
+
+        let sanitized_bundle0 = ImmutableDeserializedBundle::new(&mut packet_bundle0, None)
+            .unwrap()
+            .build_sanitized_bundle(&bank, &HashSet::default(), &mut transaction_errors)
+            .expect("sanitize bundle 0");
+        let sanitized_bundle1 = ImmutableDeserializedBundle::new(&mut packet_bundle1, None)
+            .unwrap()
+            .build_sanitized_bundle(&bank, &HashSet::default(), &mut transaction_errors)
+            .expect("sanitize bundle 1");
+
+        let locked_bundle0 = bundle_account_locker
+            .prepare_locked_bundle(&sanitized_bundle0, &bank)
+            .unwrap();
+
+        assert_eq!(
+            bundle_account_locker.write_locks(),
+            HashSet::from_iter([mint_keypair.pubkey(), kp0.pubkey()])
+        );
+        assert_eq!(
+            bundle_account_locker.read_locks(),
+            HashSet::from_iter([system_program::id()])
+        );
+
+        let locked_bundle1 = bundle_account_locker
+            .prepare_locked_bundle(&sanitized_bundle1, &bank)
+            .unwrap();
+        assert_eq!(
+            bundle_account_locker.write_locks(),
+            HashSet::from_iter([mint_keypair.pubkey(), kp0.pubkey(), kp1.pubkey()])
+        );
+        assert_eq!(
+            bundle_account_locker.read_locks(),
+            HashSet::from_iter([system_program::id()])
+        );
+
+        drop(locked_bundle0);
+        assert_eq!(
+            bundle_account_locker.write_locks(),
+            HashSet::from_iter([mint_keypair.pubkey(), kp1.pubkey()])
+        );
+        assert_eq!(
+            bundle_account_locker.read_locks(),
+            HashSet::from_iter([system_program::id()])
+        );
+
+        drop(locked_bundle1);
+        assert!(bundle_account_locker.write_locks().is_empty());
+        assert!(bundle_account_locker.read_locks().is_empty());
+    }
+}
diff --git a/core/src/bundle_stage/bundle_consumer.rs b/core/src/bundle_stage/bundle_consumer.rs
new file mode 100644
index 0000000000..83f3619fd6
--- /dev/null
+++ b/core/src/bundle_stage/bundle_consumer.rs
@@ -0,0 +1,1589 @@
+use {
+    crate::{
+        banking_stage::committer::CommitTransactionDetails,
+        bundle_stage::{
+            bundle_account_locker::{BundleAccountLocker, LockedBundle},
+            bundle_reserved_space_manager::BundleReservedSpaceManager,
+            bundle_stage_leader_metrics::BundleStageLeaderMetrics,
+            committer::Committer,
+        },
+        consensus_cache_updater::ConsensusCacheUpdater,
+        immutable_deserialized_bundle::ImmutableDeserializedBundle,
+        leader_slot_banking_stage_metrics::ProcessTransactionsSummary,
+        leader_slot_banking_stage_timing_metrics::LeaderExecuteAndCommitTimings,
+        proxy::block_engine_stage::BlockBuilderFeeInfo,
+        qos_service::QosService,
+        tip_manager::TipManager,
+        unprocessed_transaction_storage::UnprocessedTransactionStorage,
+    },
+    solana_bundle::{
+        bundle_execution::{load_and_execute_bundle, BundleExecutionMetrics},
+        BundleExecutionError, BundleExecutionResult, TipError,
+    },
+    solana_gossip::cluster_info::ClusterInfo,
+    solana_measure::{measure, measure_us},
+    solana_poh::poh_recorder::{BankStart, RecordTransactionsSummary, TransactionRecorder},
+    solana_runtime::{
+        bank::Bank, cost_model::TransactionCost, transaction_error_metrics::TransactionErrorMetrics,
+    },
+    solana_sdk::{
+        bundle::SanitizedBundle,
+        clock::{Slot, MAX_PROCESSING_AGE},
+        feature_set,
+        pubkey::Pubkey,
+        transaction::{self},
+    },
+    std::{
+        collections::HashSet,
+        sync::{Arc, Mutex},
+        time::{Duration, Instant},
+    },
+};
+
+pub struct ExecuteRecordCommitResult {
+    commit_transaction_details: Vec<CommitTransactionDetails>,
+    result: BundleExecutionResult<()>,
+    execution_metrics: BundleExecutionMetrics,
+    execute_and_commit_timings: LeaderExecuteAndCommitTimings,
+    transaction_error_counter: TransactionErrorMetrics,
+}
+
+pub struct BundleConsumer {
+    committer: Committer,
+    transaction_recorder: TransactionRecorder,
+    qos_service: QosService,
+    log_messages_bytes_limit: Option<usize>,
+
+    consensus_cache_updater: ConsensusCacheUpdater,
+
+    tip_manager: TipManager,
+    last_tip_update_slot: Slot,
+
+    blacklisted_accounts: HashSet<Pubkey>,
+
+    // Manages account locks across multiple transactions within a bundle to prevent race conditions
+    // with BankingStage
+    bundle_account_locker: BundleAccountLocker,
+
+    block_builder_fee_info: Arc<Mutex<BlockBuilderFeeInfo>>,
+
+    max_bundle_retry_duration: Duration,
+
+    cluster_info: Arc<ClusterInfo>,
+
+    reserved_space: BundleReservedSpaceManager,
+}
+
+impl BundleConsumer {
+    #[allow(clippy::too_many_arguments)]
+    pub fn new(
+        committer: Committer,
+        transaction_recorder: TransactionRecorder,
+        qos_service: QosService,
+        log_messages_bytes_limit: Option<usize>,
+        tip_manager: TipManager,
+        bundle_account_locker: BundleAccountLocker,
+        block_builder_fee_info: Arc<Mutex<BlockBuilderFeeInfo>>,
+        max_bundle_retry_duration: Duration,
+        cluster_info: Arc<ClusterInfo>,
+        reserved_space: BundleReservedSpaceManager,
+    ) -> Self {
+        Self {
+            committer,
+            transaction_recorder,
+            qos_service,
+            log_messages_bytes_limit,
+            consensus_cache_updater: ConsensusCacheUpdater::default(),
+            tip_manager,
+            // MAX because sending tips during slot 0 in tests doesn't work
+            last_tip_update_slot: u64::MAX,
+            blacklisted_accounts: HashSet::default(),
+            bundle_account_locker,
+            block_builder_fee_info,
+            max_bundle_retry_duration,
+            cluster_info,
+            reserved_space,
+        }
+    }
+
+    // A bundle is a series of transactions to be executed sequentially, atomically, and all-or-nothing.
+    // Sequentially:
+    //  - Transactions are executed in order
+    // Atomically:
+    //  - All transactions in a bundle get recoded to PoH and committed to the bank in the same slot. Account locks
+    //  for all accounts in all transactions in a bundle are held during the entire execution to remove POH record race conditions
+    //  with transactions in BankingStage.
+    // All-or-nothing:
+    //  - All transactions are committed or none. Modified state for the entire bundle isn't recorded to PoH and committed to the
+    //  bank until all transactions in the bundle have executed.
+    //
+    // Some corner cases to be aware of when working with BundleStage:
+    // A bundle is not allowed to call the Tip Payment program in a bundle (or BankingStage).
+    // - This is to avoid stealing of tips by malicious parties with bundles that crank the tip
+    // payment program and set the tip receiver to themself.
+    // A bundle is not allowed to touch consensus-related accounts
+    //  - This is to avoid stalling the voting BankingStage threads.
+    pub fn consume_buffered_bundles(
+        &mut self,
+        bank_start: &BankStart,
+        unprocessed_transaction_storage: &mut UnprocessedTransactionStorage,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+    ) {
+        self.maybe_update_blacklist(bank_start);
+        self.reserved_space.tick(&bank_start.working_bank);
+
+        let reached_end_of_slot = unprocessed_transaction_storage.process_bundles(
+            bank_start.working_bank.clone(),
+            bundle_stage_leader_metrics,
+            &self.blacklisted_accounts,
+            |bundles, bundle_stage_leader_metrics| {
+                Self::do_process_bundles(
+                    &self.bundle_account_locker,
+                    &self.tip_manager,
+                    &mut self.last_tip_update_slot,
+                    &self.cluster_info,
+                    &self.block_builder_fee_info,
+                    &self.committer,
+                    &self.transaction_recorder,
+                    &self.qos_service,
+                    &self.log_messages_bytes_limit,
+                    self.max_bundle_retry_duration,
+                    &self.reserved_space,
+                    bundles,
+                    bank_start,
+                    bundle_stage_leader_metrics,
+                )
+            },
+        );
+
+        if reached_end_of_slot {
+            bundle_stage_leader_metrics
+                .leader_slot_metrics_tracker()
+                .set_end_of_slot_unprocessed_buffer_len(
+                    unprocessed_transaction_storage.len() as u64
+                );
+        }
+    }
+
+    /// Blacklist is updated with the tip payment program + any consensus accounts.
+    fn maybe_update_blacklist(&mut self, bank_start: &BankStart) {
+        if self
+            .consensus_cache_updater
+            .maybe_update(&bank_start.working_bank)
+        {
+            self.blacklisted_accounts = self
+                .consensus_cache_updater
+                .consensus_accounts_cache()
+                .union(&HashSet::from_iter([self
+                    .tip_manager
+                    .tip_payment_program_id()]))
+                .cloned()
+                .collect();
+
+            debug!(
+                "updated blacklist with {} accounts",
+                self.blacklisted_accounts.len()
+            );
+        }
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    fn do_process_bundles(
+        bundle_account_locker: &BundleAccountLocker,
+        tip_manager: &TipManager,
+        last_tip_updated_slot: &mut Slot,
+        cluster_info: &Arc<ClusterInfo>,
+        block_builder_fee_info: &Arc<Mutex<BlockBuilderFeeInfo>>,
+        committer: &Committer,
+        recorder: &TransactionRecorder,
+        qos_service: &QosService,
+        log_messages_bytes_limit: &Option<usize>,
+        max_bundle_retry_duration: Duration,
+        reserved_space: &BundleReservedSpaceManager,
+        bundles: &[(ImmutableDeserializedBundle, SanitizedBundle)],
+        bank_start: &BankStart,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+    ) -> Vec<Result<(), BundleExecutionError>> {
+        // BundleAccountLocker holds RW locks for ALL accounts in ALL transactions within a single bundle.
+        // By pre-locking bundles before they're ready to be processed, it will prevent BankingStage from
+        // grabbing those locks so BundleStage can process as fast as possible.
+        // A LockedBundle is similar to TransactionBatch; once its dropped the locks are released.
+        #[allow(clippy::needless_collect)]
+        let (locked_bundle_results, locked_bundles_elapsed) = measure!(
+            bundles
+                .iter()
+                .map(|(_, sanitized_bundle)| {
+                    bundle_account_locker
+                        .prepare_locked_bundle(sanitized_bundle, &bank_start.working_bank)
+                })
+                .collect::<Vec<_>>(),
+            "locked_bundles_elapsed"
+        );
+        bundle_stage_leader_metrics
+            .bundle_stage_metrics_tracker()
+            .increment_locked_bundle_elapsed_us(locked_bundles_elapsed.as_us());
+
+        let (execution_results, execute_locked_bundles_elapsed) = measure!(locked_bundle_results
+            .into_iter()
+            .map(|r| match r {
+                Ok(locked_bundle) => {
+                    let (r, measure) = measure_us!(Self::process_bundle(
+                        bundle_account_locker,
+                        tip_manager,
+                        last_tip_updated_slot,
+                        cluster_info,
+                        block_builder_fee_info,
+                        committer,
+                        recorder,
+                        qos_service,
+                        log_messages_bytes_limit,
+                        max_bundle_retry_duration,
+                        reserved_space,
+                        &locked_bundle,
+                        bank_start,
+                        bundle_stage_leader_metrics,
+                    ));
+                    bundle_stage_leader_metrics
+                        .leader_slot_metrics_tracker()
+                        .increment_process_packets_transactions_us(measure);
+                    r
+                }
+                Err(_) => {
+                    Err(BundleExecutionError::LockError)
+                }
+            })
+            .collect::<Vec<_>>());
+
+        bundle_stage_leader_metrics
+            .bundle_stage_metrics_tracker()
+            .increment_execute_locked_bundles_elapsed_us(execute_locked_bundles_elapsed.as_us());
+        execution_results.iter().for_each(|result| {
+            bundle_stage_leader_metrics
+                .bundle_stage_metrics_tracker()
+                .increment_bundle_execution_result(result);
+        });
+
+        execution_results
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    fn process_bundle(
+        bundle_account_locker: &BundleAccountLocker,
+        tip_manager: &TipManager,
+        last_tip_updated_slot: &mut Slot,
+        cluster_info: &Arc<ClusterInfo>,
+        block_builder_fee_info: &Arc<Mutex<BlockBuilderFeeInfo>>,
+        committer: &Committer,
+        recorder: &TransactionRecorder,
+        qos_service: &QosService,
+        log_messages_bytes_limit: &Option<usize>,
+        max_bundle_retry_duration: Duration,
+        reserved_space: &BundleReservedSpaceManager,
+        locked_bundle: &LockedBundle,
+        bank_start: &BankStart,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+    ) -> Result<(), BundleExecutionError> {
+        if !Bank::should_bank_still_be_processing_txs(
+            &bank_start.bank_creation_time,
+            bank_start.working_bank.ns_per_slot,
+        ) {
+            return Err(BundleExecutionError::BankProcessingTimeLimitReached);
+        }
+
+        if Self::bundle_touches_tip_pdas(
+            locked_bundle.sanitized_bundle(),
+            &tip_manager.get_tip_accounts(),
+        ) && bank_start.working_bank.slot() != *last_tip_updated_slot
+        {
+            let start = Instant::now();
+            let result = Self::handle_tip_programs(
+                bundle_account_locker,
+                tip_manager,
+                cluster_info,
+                block_builder_fee_info,
+                committer,
+                recorder,
+                qos_service,
+                log_messages_bytes_limit,
+                max_bundle_retry_duration,
+                reserved_space,
+                bank_start,
+                bundle_stage_leader_metrics,
+            );
+
+            bundle_stage_leader_metrics
+                .bundle_stage_metrics_tracker()
+                .increment_change_tip_receiver_elapsed_us(start.elapsed().as_micros() as u64);
+
+            result?;
+
+            *last_tip_updated_slot = bank_start.working_bank.slot();
+        }
+
+        Self::update_qos_and_execute_record_commit_bundle(
+            committer,
+            recorder,
+            qos_service,
+            log_messages_bytes_limit,
+            max_bundle_retry_duration,
+            reserved_space,
+            locked_bundle.sanitized_bundle(),
+            bank_start,
+            bundle_stage_leader_metrics,
+        )?;
+
+        Ok(())
+    }
+
+    /// The validator needs to manage state on two programs related to tips
+    #[allow(clippy::too_many_arguments)]
+    fn handle_tip_programs(
+        bundle_account_locker: &BundleAccountLocker,
+        tip_manager: &TipManager,
+        cluster_info: &Arc<ClusterInfo>,
+        block_builder_fee_info: &Arc<Mutex<BlockBuilderFeeInfo>>,
+        committer: &Committer,
+        recorder: &TransactionRecorder,
+        qos_service: &QosService,
+        log_messages_bytes_limit: &Option<usize>,
+        max_bundle_retry_duration: Duration,
+        reserved_space: &BundleReservedSpaceManager,
+        bank_start: &BankStart,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+    ) -> Result<(), BundleExecutionError> {
+        debug!("handle_tip_programs");
+
+        // This will setup the tip payment and tip distribution program if they haven't been
+        // initialized yet, which is typically helpful for local validators. On mainnet and testnet,
+        // this code should never run.
+        let keypair = cluster_info.keypair().clone();
+        let initialize_tip_programs_bundle =
+            tip_manager.get_initialize_tip_programs_bundle(&bank_start.working_bank, &keypair);
+        if let Some(bundle) = initialize_tip_programs_bundle {
+            debug!(
+                "initializing tip programs with {} transactions, bundle id: {}",
+                bundle.transactions.len(),
+                bundle.bundle_id
+            );
+
+            let locked_init_tip_programs_bundle = bundle_account_locker
+                .prepare_locked_bundle(&bundle, &bank_start.working_bank)
+                .map_err(|_| BundleExecutionError::TipError(TipError::LockError))?;
+
+            Self::update_qos_and_execute_record_commit_bundle(
+                committer,
+                recorder,
+                qos_service,
+                log_messages_bytes_limit,
+                max_bundle_retry_duration,
+                reserved_space,
+                locked_init_tip_programs_bundle.sanitized_bundle(),
+                bank_start,
+                bundle_stage_leader_metrics,
+            )
+            .map_err(|e| {
+                bundle_stage_leader_metrics
+                    .bundle_stage_metrics_tracker()
+                    .increment_num_init_tip_account_errors(1);
+                error!(
+                    "bundle: {} error initializing tip programs: {:?}",
+                    locked_init_tip_programs_bundle.sanitized_bundle().bundle_id,
+                    e
+                );
+                BundleExecutionError::TipError(TipError::InitializeProgramsError)
+            })?;
+
+            bundle_stage_leader_metrics
+                .bundle_stage_metrics_tracker()
+                .increment_num_init_tip_account_ok(1);
+        }
+
+        // There are two frequently run internal cranks inside the jito-solana validator that have to do with managing MEV tips.
+        // One is initialize the TipDistributionAccount, which is a validator's "tip piggy bank" for an epoch
+        // The other is ensuring the tip_receiver is configured correctly to ensure tips are routed to the correct
+        // address. The validator must drain the tip accounts to the previous tip receiver before setting the tip receiver to
+        // themselves.
+
+        let kp = cluster_info.keypair().clone();
+        let tip_crank_bundle = tip_manager.get_tip_programs_crank_bundle(
+            &bank_start.working_bank,
+            &kp,
+            &block_builder_fee_info.lock().unwrap(),
+        )?;
+        debug!("tip_crank_bundle is_some: {}", tip_crank_bundle.is_some());
+
+        if let Some(bundle) = tip_crank_bundle {
+            info!(
+                "bundle id: {} cranking tip programs with {} transactions",
+                bundle.bundle_id,
+                bundle.transactions.len()
+            );
+
+            let locked_tip_crank_bundle = bundle_account_locker
+                .prepare_locked_bundle(&bundle, &bank_start.working_bank)
+                .map_err(|_| BundleExecutionError::TipError(TipError::LockError))?;
+
+            Self::update_qos_and_execute_record_commit_bundle(
+                committer,
+                recorder,
+                qos_service,
+                log_messages_bytes_limit,
+                max_bundle_retry_duration,
+                reserved_space,
+                locked_tip_crank_bundle.sanitized_bundle(),
+                bank_start,
+                bundle_stage_leader_metrics,
+            )
+            .map_err(|e| {
+                bundle_stage_leader_metrics
+                    .bundle_stage_metrics_tracker()
+                    .increment_num_change_tip_receiver_errors(1);
+                error!(
+                    "bundle: {} error cranking tip programs: {:?}",
+                    locked_tip_crank_bundle.sanitized_bundle().bundle_id,
+                    e
+                );
+                BundleExecutionError::TipError(TipError::CrankTipError)
+            })?;
+
+            bundle_stage_leader_metrics
+                .bundle_stage_metrics_tracker()
+                .increment_num_change_tip_receiver_ok(1);
+        }
+
+        Ok(())
+    }
+
+    /// Reserves space for the entire bundle up-front to ensure the entire bundle can execute.
+    /// Rolls back the reserved space if there's not enough blockspace for all transactions in the bundle.
+    fn reserve_bundle_blockspace(
+        qos_service: &QosService,
+        reserved_space: &BundleReservedSpaceManager,
+        sanitized_bundle: &SanitizedBundle,
+        bank: &Arc<Bank>,
+    ) -> BundleExecutionResult<(Vec<transaction::Result<TransactionCost>>, usize)> {
+        let mut write_cost_tracker = bank.write_cost_tracker().unwrap();
+
+        // set the block cost limit to the original block cost limit, run the select + accumulate
+        // then reset back to the expected block cost limit. this allows bundle stage to potentially
+        // increase block_compute_limits, allocate the space, and reset the block_cost_limits to
+        // the reserved space without BankingStage racing to allocate this extra reserved space
+        write_cost_tracker.set_block_cost_limit(reserved_space.block_cost_limit());
+        let (transaction_qos_cost_results, cost_model_throttled_transactions_count) = qos_service
+            .select_and_accumulate_transaction_costs(
+                bank,
+                &mut write_cost_tracker,
+                &sanitized_bundle.transactions,
+                std::iter::repeat(Ok(())),
+            );
+        write_cost_tracker.set_block_cost_limit(reserved_space.expected_block_cost_limits(bank));
+        drop(write_cost_tracker);
+
+        // rollback all transaction costs if it can't fit and
+        if transaction_qos_cost_results.iter().any(|c| c.is_err()) {
+            QosService::remove_costs(transaction_qos_cost_results.iter(), None, bank);
+            return Err(BundleExecutionError::ExceedsCostModel);
+        }
+
+        Ok((
+            transaction_qos_cost_results,
+            cost_model_throttled_transactions_count,
+        ))
+    }
+
+    fn update_qos_and_execute_record_commit_bundle(
+        committer: &Committer,
+        recorder: &TransactionRecorder,
+        qos_service: &QosService,
+        log_messages_bytes_limit: &Option<usize>,
+        max_bundle_retry_duration: Duration,
+        reserved_space: &BundleReservedSpaceManager,
+        sanitized_bundle: &SanitizedBundle,
+        bank_start: &BankStart,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+    ) -> BundleExecutionResult<()> {
+        debug!(
+            "bundle: {} reserving blockspace for {} transactions",
+            sanitized_bundle.bundle_id,
+            sanitized_bundle.transactions.len()
+        );
+
+        let (
+            (transaction_qos_cost_results, _cost_model_throttled_transactions_count),
+            cost_model_elapsed_us,
+        ) = measure_us!(Self::reserve_bundle_blockspace(
+            qos_service,
+            reserved_space,
+            sanitized_bundle,
+            &bank_start.working_bank
+        )?);
+
+        debug!(
+            "bundle: {} executing, recording, and committing",
+            sanitized_bundle.bundle_id
+        );
+
+        let (result, process_transactions_us) = measure_us!(Self::execute_record_commit_bundle(
+            committer,
+            recorder,
+            log_messages_bytes_limit,
+            max_bundle_retry_duration,
+            sanitized_bundle,
+            bank_start,
+        ));
+
+        bundle_stage_leader_metrics
+            .bundle_stage_metrics_tracker()
+            .increment_num_execution_retries(result.execution_metrics.num_retries);
+        bundle_stage_leader_metrics
+            .leader_slot_metrics_tracker()
+            .accumulate_transaction_errors(&result.transaction_error_counter);
+        bundle_stage_leader_metrics
+            .leader_slot_metrics_tracker()
+            .increment_process_transactions_us(process_transactions_us);
+
+        let (cu, us) = result
+            .execute_and_commit_timings
+            .execute_timings
+            .accumulate_execute_units_and_time();
+        qos_service.accumulate_actual_execute_cu(cu);
+        qos_service.accumulate_actual_execute_time(us);
+
+        let num_committed = result
+            .commit_transaction_details
+            .iter()
+            .filter(|c| matches!(c, CommitTransactionDetails::Committed { .. }))
+            .count();
+        bundle_stage_leader_metrics
+            .leader_slot_metrics_tracker()
+            .accumulate_process_transactions_summary(&ProcessTransactionsSummary {
+                reached_max_poh_height: matches!(
+                    result.result,
+                    Err(BundleExecutionError::BankProcessingTimeLimitReached)
+                        | Err(BundleExecutionError::PohRecordError(_))
+                ),
+                transactions_attempted_execution_count: sanitized_bundle.transactions.len(),
+                committed_transactions_count: num_committed,
+                // NOTE: this assumes that bundles are committed all-or-nothing
+                committed_transactions_with_successful_result_count: num_committed,
+                failed_commit_count: 0,
+                retryable_transaction_indexes: vec![],
+                cost_model_throttled_transactions_count: 0,
+                cost_model_us: cost_model_elapsed_us,
+                execute_and_commit_timings: result.execute_and_commit_timings,
+                error_counters: result.transaction_error_counter,
+            });
+
+        match result.result {
+            Ok(_) => {
+                // it's assumed that all transactions in the bundle executed, can update QoS
+                if !bank_start
+                    .working_bank
+                    .feature_set
+                    .is_active(&feature_set::apply_cost_tracker_during_replay::id())
+                {
+                    QosService::update_costs(
+                        transaction_qos_cost_results.iter(),
+                        Some(&result.commit_transaction_details),
+                        &bank_start.working_bank,
+                    );
+                }
+
+                qos_service.report_metrics(bank_start.working_bank.slot());
+                Ok(())
+            }
+            Err(e) => {
+                // on bundle failure, none of the transactions are committed, so need to revert
+                // all compute reserved
+                QosService::remove_costs(
+                    transaction_qos_cost_results.iter(),
+                    None,
+                    &bank_start.working_bank,
+                );
+                qos_service.report_metrics(bank_start.working_bank.slot());
+
+                Err(e)
+            }
+        }
+    }
+
+    fn execute_record_commit_bundle(
+        committer: &Committer,
+        recorder: &TransactionRecorder,
+        log_messages_bytes_limit: &Option<usize>,
+        max_bundle_retry_duration: Duration,
+        sanitized_bundle: &SanitizedBundle,
+        bank_start: &BankStart,
+    ) -> ExecuteRecordCommitResult {
+        let transaction_status_sender_enabled = committer.transaction_status_sender_enabled();
+
+        let mut execute_and_commit_timings = LeaderExecuteAndCommitTimings::default();
+
+        debug!("bundle: {} executing", sanitized_bundle.bundle_id);
+        let default_accounts = vec![None; sanitized_bundle.transactions.len()];
+        let mut bundle_execution_results = load_and_execute_bundle(
+            &bank_start.working_bank,
+            sanitized_bundle,
+            MAX_PROCESSING_AGE,
+            &max_bundle_retry_duration,
+            transaction_status_sender_enabled,
+            transaction_status_sender_enabled,
+            transaction_status_sender_enabled,
+            transaction_status_sender_enabled,
+            log_messages_bytes_limit,
+            false,
+            None,
+            &default_accounts,
+            &default_accounts,
+        );
+
+        let execution_metrics = bundle_execution_results.metrics();
+
+        execute_and_commit_timings.collect_balances_us = execution_metrics.collect_balances_us;
+        execute_and_commit_timings.load_execute_us = execution_metrics.load_execute_us;
+        execute_and_commit_timings
+            .execute_timings
+            .accumulate(&execution_metrics.execute_timings);
+
+        let mut transaction_error_counter = TransactionErrorMetrics::default();
+        bundle_execution_results
+            .bundle_transaction_results()
+            .iter()
+            .for_each(|r| {
+                transaction_error_counter
+                    .accumulate(&r.load_and_execute_transactions_output().error_counters);
+            });
+
+        debug!(
+            "bundle: {} executed, is_ok: {}",
+            sanitized_bundle.bundle_id,
+            bundle_execution_results.result().is_ok()
+        );
+
+        // don't commit bundle if failure executing any part of the bundle
+        if let Err(e) = bundle_execution_results.result() {
+            return ExecuteRecordCommitResult {
+                commit_transaction_details: vec![],
+                result: Err(e.clone().into()),
+                execution_metrics,
+                execute_and_commit_timings,
+                transaction_error_counter,
+            };
+        }
+
+        let (executed_batches, execution_results_to_transactions_us) =
+            measure_us!(bundle_execution_results.executed_transaction_batches());
+
+        debug!(
+            "bundle: {} recording {} batches of {:?} transactions",
+            sanitized_bundle.bundle_id,
+            executed_batches.len(),
+            executed_batches
+                .iter()
+                .map(|b| b.len())
+                .collect::<Vec<usize>>()
+        );
+
+        let (freeze_lock, freeze_lock_us) = measure_us!(bank_start.working_bank.freeze_lock());
+        execute_and_commit_timings.freeze_lock_us = freeze_lock_us;
+
+        let (
+            RecordTransactionsSummary {
+                result: record_transactions_result,
+                record_transactions_timings,
+                starting_transaction_index,
+            },
+            record_us,
+        ) = measure_us!(
+            recorder.record_transactions(bank_start.working_bank.slot(), executed_batches)
+        );
+
+        execute_and_commit_timings.record_us = record_us;
+        execute_and_commit_timings.record_transactions_timings = record_transactions_timings;
+        execute_and_commit_timings
+            .record_transactions_timings
+            .execution_results_to_transactions_us = execution_results_to_transactions_us;
+
+        debug!(
+            "bundle: {} record result: {}",
+            sanitized_bundle.bundle_id,
+            record_transactions_result.is_ok()
+        );
+
+        // don't commit bundle if failed to record
+        if let Err(e) = record_transactions_result {
+            return ExecuteRecordCommitResult {
+                commit_transaction_details: vec![],
+                result: Err(e.into()),
+                execution_metrics,
+                execute_and_commit_timings,
+                transaction_error_counter,
+            };
+        }
+
+        // note: execute_and_commit_timings.commit_us handled inside this function
+        let (commit_us, commit_bundle_details) = committer.commit_bundle(
+            &mut bundle_execution_results,
+            starting_transaction_index,
+            &bank_start.working_bank,
+            &mut execute_and_commit_timings,
+        );
+        execute_and_commit_timings.commit_us = commit_us;
+
+        drop(freeze_lock);
+
+        // commit_bundle_details contains transactions that were and were not committed
+        // given the current implementation only executes, records, and commits bundles
+        // where all transactions executed, we can filter out the non-committed
+        // TODO (LB): does this make more sense in commit_bundle for future when failing bundles are accepted?
+        let commit_transaction_details = commit_bundle_details
+            .commit_transaction_details
+            .into_iter()
+            .flat_map(|commit_details| {
+                commit_details
+                    .into_iter()
+                    .filter(|d| matches!(d, CommitTransactionDetails::Committed { .. }))
+            })
+            .collect();
+        debug!(
+            "bundle: {} commit details: {:?}",
+            sanitized_bundle.bundle_id, commit_transaction_details
+        );
+
+        ExecuteRecordCommitResult {
+            commit_transaction_details,
+            result: Ok(()),
+            execution_metrics,
+            execute_and_commit_timings,
+            transaction_error_counter,
+        }
+    }
+
+    /// Returns true if any of the transactions in a bundle mention one of the tip PDAs
+    fn bundle_touches_tip_pdas(bundle: &SanitizedBundle, tip_pdas: &HashSet<Pubkey>) -> bool {
+        bundle.transactions.iter().any(|tx| {
+            tx.message()
+                .account_keys()
+                .iter()
+                .any(|a| tip_pdas.contains(a))
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {
+        crate::{
+            bundle_stage::{
+                bundle_account_locker::BundleAccountLocker, bundle_consumer::BundleConsumer,
+                bundle_packet_deserializer::BundlePacketDeserializer,
+                bundle_reserved_space_manager::BundleReservedSpaceManager,
+                bundle_stage_leader_metrics::BundleStageLeaderMetrics, committer::Committer,
+            },
+            packet_bundle::PacketBundle,
+            proxy::block_engine_stage::BlockBuilderFeeInfo,
+            qos_service::QosService,
+            tip_manager::{TipDistributionAccountConfig, TipManager, TipManagerConfig},
+            unprocessed_transaction_storage::UnprocessedTransactionStorage,
+        },
+        crossbeam_channel::{unbounded, Receiver},
+        jito_tip_distribution::sdk::derive_tip_distribution_account_address,
+        rand::{thread_rng, RngCore},
+        solana_gossip::{cluster_info::ClusterInfo, contact_info::ContactInfo},
+        solana_ledger::{
+            blockstore::Blockstore, genesis_utils::create_genesis_config,
+            get_tmp_ledger_path_auto_delete, leader_schedule_cache::LeaderScheduleCache,
+        },
+        solana_perf::packet::PacketBatch,
+        solana_poh::{
+            poh_recorder::{PohRecorder, Record, WorkingBankEntry},
+            poh_service::PohService,
+        },
+        solana_program_test::programs::spl_programs,
+        solana_runtime::{
+            bank::Bank,
+            block_cost_limits::MAX_BLOCK_UNITS,
+            cost_model::CostModel,
+            genesis_utils::{create_genesis_config_with_leader_ex, GenesisConfigInfo},
+            prioritization_fee_cache::PrioritizationFeeCache,
+            transaction_error_metrics::TransactionErrorMetrics,
+        },
+        solana_sdk::{
+            bundle::{derive_bundle_id, SanitizedBundle},
+            clock::MAX_PROCESSING_AGE,
+            feature_set::delay_visibility_of_program_deployment,
+            fee_calculator::{FeeRateGovernor, DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE},
+            genesis_config::ClusterType,
+            hash::Hash,
+            native_token::sol_to_lamports,
+            packet::Packet,
+            poh_config::PohConfig,
+            pubkey::Pubkey,
+            rent::Rent,
+            signature::{Keypair, Signer},
+            system_transaction::transfer,
+            transaction::{SanitizedTransaction, TransactionError, VersionedTransaction},
+            vote::state::VoteState,
+        },
+        solana_streamer::socket::SocketAddrSpace,
+        std::{
+            collections::{HashSet, VecDeque},
+            str::FromStr,
+            sync::{
+                atomic::{AtomicBool, Ordering},
+                Arc, Mutex, RwLock,
+            },
+            thread::{Builder, JoinHandle},
+            time::Duration,
+        },
+    };
+
+    struct TestFixture {
+        genesis_config_info: GenesisConfigInfo,
+        leader_keypair: Keypair,
+        bank: Arc<Bank>,
+        exit: Arc<AtomicBool>,
+        poh_recorder: Arc<RwLock<PohRecorder>>,
+        poh_simulator: JoinHandle<()>,
+        entry_receiver: Receiver<WorkingBankEntry>,
+    }
+
+    pub(crate) fn simulate_poh(
+        record_receiver: Receiver<Record>,
+        poh_recorder: &Arc<RwLock<PohRecorder>>,
+    ) -> JoinHandle<()> {
+        let poh_recorder = poh_recorder.clone();
+        let is_exited = poh_recorder.read().unwrap().is_exited.clone();
+        let tick_producer = Builder::new()
+            .name("solana-simulate_poh".to_string())
+            .spawn(move || loop {
+                PohService::read_record_receiver_and_process(
+                    &poh_recorder,
+                    &record_receiver,
+                    Duration::from_millis(10),
+                );
+                if is_exited.load(Ordering::Relaxed) {
+                    break;
+                }
+            });
+        tick_producer.unwrap()
+    }
+
+    pub fn create_test_recorder(
+        bank: &Arc<Bank>,
+        blockstore: Arc<Blockstore>,
+        poh_config: Option<PohConfig>,
+        leader_schedule_cache: Option<Arc<LeaderScheduleCache>>,
+    ) -> (
+        Arc<AtomicBool>,
+        Arc<RwLock<PohRecorder>>,
+        JoinHandle<()>,
+        Receiver<WorkingBankEntry>,
+    ) {
+        let leader_schedule_cache = match leader_schedule_cache {
+            Some(provided_cache) => provided_cache,
+            None => Arc::new(LeaderScheduleCache::new_from_bank(bank)),
+        };
+        let exit = Arc::new(AtomicBool::new(false));
+        let poh_config = poh_config.unwrap_or_default();
+        let (mut poh_recorder, entry_receiver, record_receiver) = PohRecorder::new(
+            bank.tick_height(),
+            bank.last_blockhash(),
+            bank.clone(),
+            Some((4, 4)),
+            bank.ticks_per_slot(),
+            &Pubkey::default(),
+            blockstore,
+            &leader_schedule_cache,
+            &poh_config,
+            exit.clone(),
+        );
+        poh_recorder.set_bank(bank.clone(), false);
+
+        let poh_recorder = Arc::new(RwLock::new(poh_recorder));
+        let poh_simulator = simulate_poh(record_receiver, &poh_recorder);
+
+        (exit, poh_recorder, poh_simulator, entry_receiver)
+    }
+
+    fn create_test_fixture(mint_sol: u64) -> TestFixture {
+        let mint_keypair = Keypair::new();
+        let leader_keypair = Keypair::new();
+        let voting_keypair = Keypair::new();
+
+        let rent = Rent::default();
+
+        let mut genesis_config = create_genesis_config_with_leader_ex(
+            sol_to_lamports(mint_sol as f64),
+            &mint_keypair.pubkey(),
+            &leader_keypair.pubkey(),
+            &voting_keypair.pubkey(),
+            &solana_sdk::pubkey::new_rand(),
+            rent.minimum_balance(VoteState::size_of()) + sol_to_lamports(1_000_000.0),
+            sol_to_lamports(1_000_000.0),
+            FeeRateGovernor {
+                // Initialize with a non-zero fee
+                lamports_per_signature: DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE / 2,
+                ..FeeRateGovernor::default()
+            },
+            rent, // most tests don't expect rent
+            ClusterType::Development,
+            spl_programs(&rent),
+        );
+        genesis_config.ticks_per_slot *= 8;
+
+        // workaround for https://github.com/solana-labs/solana/issues/30085
+        // the test can deploy and use spl_programs in the genensis slot without waiting for the next one
+        let mut bank = Bank::new_for_tests(&genesis_config);
+        bank.deactivate_feature(&delay_visibility_of_program_deployment::id());
+        let bank = Arc::new(bank);
+
+        let ledger_path = get_tmp_ledger_path_auto_delete!();
+        let blockstore = Arc::new(
+            Blockstore::open(ledger_path.path())
+                .expect("Expected to be able to open database ledger"),
+        );
+
+        let (exit, poh_recorder, poh_simulator, entry_receiver) =
+            create_test_recorder(&bank, blockstore, Some(PohConfig::default()), None);
+
+        let validator_pubkey = voting_keypair.pubkey();
+        TestFixture {
+            genesis_config_info: GenesisConfigInfo {
+                genesis_config,
+                mint_keypair,
+                voting_keypair,
+                validator_pubkey,
+            },
+            leader_keypair,
+            bank,
+            exit,
+            poh_recorder,
+            poh_simulator,
+            entry_receiver,
+        }
+    }
+
+    fn make_random_overlapping_bundles(
+        mint_keypair: &Keypair,
+        num_bundles: usize,
+        num_packets_per_bundle: usize,
+        hash: Hash,
+        max_transfer_amount: u64,
+    ) -> Vec<PacketBundle> {
+        let mut rng = thread_rng();
+
+        (0..num_bundles)
+            .map(|_| {
+                let transfers: Vec<_> = (0..num_packets_per_bundle)
+                    .map(|_| {
+                        VersionedTransaction::from(transfer(
+                            mint_keypair,
+                            &mint_keypair.pubkey(),
+                            rng.next_u64() % max_transfer_amount,
+                            hash,
+                        ))
+                    })
+                    .collect();
+                let bundle_id = derive_bundle_id(&transfers);
+
+                PacketBundle {
+                    batch: PacketBatch::new(
+                        transfers
+                            .iter()
+                            .map(|tx| Packet::from_data(None, tx).unwrap())
+                            .collect(),
+                    ),
+                    bundle_id,
+                }
+            })
+            .collect()
+    }
+
+    fn get_tip_manager(vote_account: &Pubkey) -> TipManager {
+        TipManager::new(TipManagerConfig {
+            tip_payment_program_id: Pubkey::from_str("T1pyyaTNZsKv2WcRAB8oVnk93mLJw2XzjtVYqCsaHqt")
+                .unwrap(),
+            tip_distribution_program_id: Pubkey::from_str(
+                "4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7",
+            )
+            .unwrap(),
+            tip_distribution_account_config: TipDistributionAccountConfig {
+                merkle_root_upload_authority: Pubkey::new_unique(),
+                vote_account: *vote_account,
+                commission_bps: 10,
+            },
+        })
+    }
+
+    /// Happy-path bundle execution w/ no tip management
+    #[test]
+    fn test_bundle_no_tip_success() {
+        solana_logger::setup();
+        let TestFixture {
+            genesis_config_info,
+            leader_keypair,
+            bank,
+            exit,
+            poh_recorder,
+            poh_simulator,
+            entry_receiver,
+        } = create_test_fixture(1_000_000);
+        let recorder = poh_recorder.read().unwrap().new_recorder();
+
+        let status = poh_recorder.read().unwrap().reached_leader_slot();
+        info!("status: {:?}", status);
+
+        let (replay_vote_sender, _replay_vote_receiver) = unbounded();
+        let committer = Committer::new(
+            None,
+            replay_vote_sender,
+            Arc::new(PrioritizationFeeCache::new(0u64)),
+        );
+
+        let block_builder_pubkey = Pubkey::new_unique();
+        let tip_manager = get_tip_manager(&genesis_config_info.voting_keypair.pubkey());
+        let block_builder_info = Arc::new(Mutex::new(BlockBuilderFeeInfo {
+            block_builder: block_builder_pubkey,
+            block_builder_commission: 10,
+        }));
+
+        let cluster_info = Arc::new(ClusterInfo::new(
+            ContactInfo::new(leader_keypair.pubkey(), 0, 0),
+            Arc::new(leader_keypair),
+            SocketAddrSpace::new(true),
+        ));
+
+        let mut consumer = BundleConsumer::new(
+            committer,
+            recorder,
+            QosService::new(1),
+            None,
+            tip_manager,
+            BundleAccountLocker::default(),
+            block_builder_info,
+            Duration::from_secs(10),
+            cluster_info,
+            BundleReservedSpaceManager::new(
+                MAX_BLOCK_UNITS,
+                3_000_000,
+                poh_recorder
+                    .read()
+                    .unwrap()
+                    .ticks_per_slot()
+                    .saturating_mul(8)
+                    .saturating_div(10),
+            ),
+        );
+
+        let bank_start = poh_recorder.read().unwrap().bank_start().unwrap();
+
+        let mut bundle_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(10),
+            VecDeque::with_capacity(10),
+        );
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(1);
+
+        let mut packet_bundles = make_random_overlapping_bundles(
+            &genesis_config_info.mint_keypair,
+            1,
+            3,
+            genesis_config_info.genesis_config.hash(),
+            10_000,
+        );
+        let deserialized_bundle = BundlePacketDeserializer::deserialize_bundle(
+            packet_bundles.get_mut(0).unwrap(),
+            false,
+            None,
+        )
+        .unwrap();
+        let mut error_metrics = TransactionErrorMetrics::default();
+        let sanitized_bundle = deserialized_bundle
+            .build_sanitized_bundle(
+                &bank_start.working_bank,
+                &HashSet::default(),
+                &mut error_metrics,
+            )
+            .unwrap();
+
+        let summary = bundle_storage.insert_bundles(vec![deserialized_bundle]);
+        assert_eq!(
+            summary.num_packets_inserted,
+            sanitized_bundle.transactions.len()
+        );
+        assert_eq!(summary.num_bundles_dropped, 0);
+        assert_eq!(summary.num_bundles_inserted, 1);
+
+        consumer.consume_buffered_bundles(
+            &bank_start,
+            &mut bundle_storage,
+            &mut bundle_stage_leader_metrics,
+        );
+
+        let mut transactions = Vec::new();
+        while let Ok(WorkingBankEntry {
+            bank: wbe_bank,
+            entries_ticks,
+        }) = entry_receiver.recv()
+        {
+            assert_eq!(bank.slot(), wbe_bank.slot());
+            for (entry, _) in entries_ticks {
+                if !entry.transactions.is_empty() {
+                    // transactions in this test are all overlapping, so each entry will contain 1 transaction
+                    assert_eq!(entry.transactions.len(), 1);
+                    transactions.extend(entry.transactions);
+                }
+            }
+            if transactions.len() == sanitized_bundle.transactions.len() {
+                break;
+            }
+        }
+
+        let bundle_versioned_transactions: Vec<_> = sanitized_bundle
+            .transactions
+            .iter()
+            .map(|tx| tx.to_versioned_transaction())
+            .collect();
+        assert_eq!(transactions, bundle_versioned_transactions);
+
+        let check_results = bank.check_transactions(
+            &sanitized_bundle.transactions,
+            &vec![Ok(()); sanitized_bundle.transactions.len()],
+            MAX_PROCESSING_AGE,
+            &mut error_metrics,
+        );
+        assert_eq!(
+            check_results,
+            vec![
+                (Err(TransactionError::AlreadyProcessed), None);
+                sanitized_bundle.transactions.len()
+            ]
+        );
+
+        poh_recorder
+            .write()
+            .unwrap()
+            .is_exited
+            .store(true, Ordering::Relaxed);
+        exit.store(true, Ordering::Relaxed);
+        poh_simulator.join().unwrap();
+        // TODO (LB): cleanup blockstore
+    }
+
+    /// Happy-path bundle execution to ensure tip management works.
+    /// Tip management involves cranking setup bundles before executing the test bundle
+    #[test]
+    fn test_bundle_tip_program_setup_success() {
+        solana_logger::setup();
+        let TestFixture {
+            genesis_config_info,
+            leader_keypair,
+            bank,
+            exit,
+            poh_recorder,
+            poh_simulator,
+            entry_receiver,
+        } = create_test_fixture(1_000_000);
+        let recorder = poh_recorder.read().unwrap().new_recorder();
+
+        let (replay_vote_sender, _replay_vote_receiver) = unbounded();
+        let committer = Committer::new(
+            None,
+            replay_vote_sender,
+            Arc::new(PrioritizationFeeCache::new(0u64)),
+        );
+
+        let block_builder_pubkey = Pubkey::new_unique();
+        let tip_manager = get_tip_manager(&genesis_config_info.voting_keypair.pubkey());
+        let block_builder_info = Arc::new(Mutex::new(BlockBuilderFeeInfo {
+            block_builder: block_builder_pubkey,
+            block_builder_commission: 10,
+        }));
+
+        let cluster_info = Arc::new(ClusterInfo::new(
+            ContactInfo::new(leader_keypair.pubkey(), 0, 0),
+            Arc::new(leader_keypair),
+            SocketAddrSpace::new(true),
+        ));
+
+        let mut consumer = BundleConsumer::new(
+            committer,
+            recorder,
+            QosService::new(1),
+            None,
+            tip_manager.clone(),
+            BundleAccountLocker::default(),
+            block_builder_info,
+            Duration::from_secs(10),
+            cluster_info.clone(),
+            BundleReservedSpaceManager::new(
+                MAX_BLOCK_UNITS,
+                3_000_000,
+                poh_recorder
+                    .read()
+                    .unwrap()
+                    .ticks_per_slot()
+                    .saturating_mul(8)
+                    .saturating_div(10),
+            ),
+        );
+
+        let bank_start = poh_recorder.read().unwrap().bank_start().unwrap();
+
+        let mut bundle_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(10),
+            VecDeque::with_capacity(10),
+        );
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(1);
+        // MAIN LOGIC
+
+        // a bundle that tips the tip program
+        let tip_accounts = tip_manager.get_tip_accounts();
+        let tip_account = tip_accounts.iter().collect::<Vec<_>>()[0];
+        let mut packet_bundle = PacketBundle {
+            batch: PacketBatch::new(vec![Packet::from_data(
+                None,
+                transfer(
+                    &genesis_config_info.mint_keypair,
+                    tip_account,
+                    1,
+                    genesis_config_info.genesis_config.hash(),
+                ),
+            )
+            .unwrap()]),
+            bundle_id: "test_transfer".to_string(),
+        };
+
+        let deserialized_bundle =
+            BundlePacketDeserializer::deserialize_bundle(&mut packet_bundle, false, None).unwrap();
+        let mut error_metrics = TransactionErrorMetrics::default();
+        let sanitized_bundle = deserialized_bundle
+            .build_sanitized_bundle(
+                &bank_start.working_bank,
+                &HashSet::default(),
+                &mut error_metrics,
+            )
+            .unwrap();
+
+        let summary = bundle_storage.insert_bundles(vec![deserialized_bundle]);
+        assert_eq!(summary.num_bundles_inserted, 1);
+        assert_eq!(summary.num_packets_inserted, 1);
+        assert_eq!(summary.num_bundles_dropped, 0);
+
+        consumer.consume_buffered_bundles(
+            &bank_start,
+            &mut bundle_storage,
+            &mut bundle_stage_leader_metrics,
+        );
+
+        // its expected there are 3 transactions. One to initialize the tip program configuration, one to change the tip receiver,
+        // and another with the tip
+
+        let mut transactions = Vec::new();
+        while let Ok(WorkingBankEntry {
+            bank: wbe_bank,
+            entries_ticks,
+        }) = entry_receiver.recv()
+        {
+            assert_eq!(bank.slot(), wbe_bank.slot());
+            transactions.extend(entries_ticks.into_iter().flat_map(|(e, _)| e.transactions));
+            if transactions.len() == 5 {
+                break;
+            }
+        }
+
+        // tip management on the first bundle involves:
+        // calling initialize on the tip payment and tip distribution programs
+        // creating the tip distribution account for this validator's epoch (the MEV piggy bank)
+        // changing the tip receiver and block builder tx
+        // the original transfer that was sent
+        let keypair = cluster_info.keypair().clone();
+
+        assert_eq!(
+            transactions[0],
+            tip_manager
+                .initialize_tip_payment_program_tx(bank.last_blockhash(), &keypair)
+                .to_versioned_transaction()
+        );
+        assert_eq!(
+            transactions[1],
+            tip_manager
+                .initialize_tip_distribution_config_tx(bank.last_blockhash(), &keypair)
+                .to_versioned_transaction()
+        );
+        assert_eq!(
+            transactions[2],
+            tip_manager
+                .initialize_tip_distribution_account_tx(
+                    bank.last_blockhash(),
+                    bank.epoch(),
+                    &keypair
+                )
+                .to_versioned_transaction()
+        );
+        // the first tip receiver + block builder are the initializer (keypair.pubkey()) as set by the
+        // TipPayment program during initialization
+        assert_eq!(
+            transactions[3],
+            tip_manager
+                .build_change_tip_receiver_and_block_builder_tx(
+                    &keypair.pubkey(),
+                    &derive_tip_distribution_account_address(
+                        &tip_manager.tip_distribution_program_id(),
+                        &genesis_config_info.validator_pubkey,
+                        bank_start.working_bank.epoch()
+                    )
+                    .0,
+                    &bank_start.working_bank,
+                    &keypair,
+                    &keypair.pubkey(),
+                    &block_builder_pubkey,
+                    10
+                )
+                .to_versioned_transaction()
+        );
+        assert_eq!(
+            transactions[4],
+            sanitized_bundle.transactions[0].to_versioned_transaction()
+        );
+
+        poh_recorder
+            .write()
+            .unwrap()
+            .is_exited
+            .store(true, Ordering::Relaxed);
+        exit.store(true, Ordering::Relaxed);
+        poh_simulator.join().unwrap();
+    }
+
+    #[test]
+    fn test_handle_tip_programs() {
+        solana_logger::setup();
+        let TestFixture {
+            genesis_config_info,
+            leader_keypair,
+            bank,
+            exit,
+            poh_recorder,
+            poh_simulator,
+            entry_receiver,
+        } = create_test_fixture(1_000_000);
+        let recorder = poh_recorder.read().unwrap().new_recorder();
+
+        let (replay_vote_sender, _replay_vote_receiver) = unbounded();
+        let committer = Committer::new(
+            None,
+            replay_vote_sender,
+            Arc::new(PrioritizationFeeCache::new(0u64)),
+        );
+
+        let block_builder_pubkey = Pubkey::new_unique();
+        let tip_manager = get_tip_manager(&genesis_config_info.voting_keypair.pubkey());
+        let block_builder_info = Arc::new(Mutex::new(BlockBuilderFeeInfo {
+            block_builder: block_builder_pubkey,
+            block_builder_commission: 10,
+        }));
+
+        let cluster_info = Arc::new(ClusterInfo::new(
+            ContactInfo::new(leader_keypair.pubkey(), 0, 0),
+            Arc::new(leader_keypair),
+            SocketAddrSpace::new(true),
+        ));
+
+        let bank_start = poh_recorder.read().unwrap().bank_start().unwrap();
+
+        let reserved_ticks = bank.max_tick_height().saturating_mul(8).saturating_div(10);
+
+        // The first 80% of the block, based on poh ticks, has `preallocated_bundle_cost` less compute units.
+        // The last 20% has has full compute so blockspace is maximized if BundleStage is idle.
+        let reserved_space =
+            BundleReservedSpaceManager::new(MAX_BLOCK_UNITS, 3_000_000, reserved_ticks);
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(1);
+        assert_matches!(
+            BundleConsumer::handle_tip_programs(
+                &BundleAccountLocker::default(),
+                &tip_manager,
+                &cluster_info,
+                &block_builder_info,
+                &committer,
+                &recorder,
+                &QosService::new(1),
+                &None,
+                Duration::from_secs(10),
+                &reserved_space,
+                &bank_start,
+                &mut bundle_stage_leader_metrics
+            ),
+            Ok(())
+        );
+
+        let mut transactions = Vec::new();
+        while let Ok(WorkingBankEntry {
+            bank: wbe_bank,
+            entries_ticks,
+        }) = entry_receiver.recv()
+        {
+            assert_eq!(bank.slot(), wbe_bank.slot());
+            transactions.extend(entries_ticks.into_iter().flat_map(|(e, _)| e.transactions));
+            if transactions.len() == 4 {
+                break;
+            }
+        }
+
+        let keypair = cluster_info.keypair().clone();
+        // expect to see initialize tip payment program, tip distribution program, initialize tip distribution account, change tip receiver + change block builder
+        assert_eq!(
+            transactions[0],
+            tip_manager
+                .initialize_tip_payment_program_tx(bank.last_blockhash(), &keypair)
+                .to_versioned_transaction()
+        );
+        assert_eq!(
+            transactions[1],
+            tip_manager
+                .initialize_tip_distribution_config_tx(bank.last_blockhash(), &keypair)
+                .to_versioned_transaction()
+        );
+        assert_eq!(
+            transactions[2],
+            tip_manager
+                .initialize_tip_distribution_account_tx(
+                    bank.last_blockhash(),
+                    bank.epoch(),
+                    &keypair
+                )
+                .to_versioned_transaction()
+        );
+        // the first tip receiver + block builder are the initializer (keypair.pubkey()) as set by the
+        // TipPayment program during initialization
+        assert_eq!(
+            transactions[3],
+            tip_manager
+                .build_change_tip_receiver_and_block_builder_tx(
+                    &keypair.pubkey(),
+                    &derive_tip_distribution_account_address(
+                        &tip_manager.tip_distribution_program_id(),
+                        &genesis_config_info.validator_pubkey,
+                        bank_start.working_bank.epoch()
+                    )
+                    .0,
+                    &bank_start.working_bank,
+                    &keypair,
+                    &keypair.pubkey(),
+                    &block_builder_pubkey,
+                    10
+                )
+                .to_versioned_transaction()
+        );
+
+        poh_recorder
+            .write()
+            .unwrap()
+            .is_exited
+            .store(true, Ordering::Relaxed);
+        exit.store(true, Ordering::Relaxed);
+        poh_simulator.join().unwrap();
+    }
+
+    #[test]
+    fn test_reserve_bundle_blockspace_success() {
+        let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10);
+        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
+
+        let keypair1 = Keypair::new();
+        let keypair2 = Keypair::new();
+        let transfer_tx = SanitizedTransaction::from_transaction_for_tests(transfer(
+            &keypair1,
+            &keypair2.pubkey(),
+            1,
+            bank.parent_hash(),
+        ));
+        let sanitized_bundle = SanitizedBundle {
+            transactions: vec![transfer_tx],
+            bundle_id: String::default(),
+        };
+
+        let transfer_cost =
+            CostModel::calculate_cost(&sanitized_bundle.transactions[0], &bank.feature_set);
+
+        let qos_service = QosService::new(1);
+        let reserved_ticks = bank.max_tick_height().saturating_mul(8).saturating_div(10);
+
+        // The first 80% of the block, based on poh ticks, has `preallocated_bundle_cost` less compute units.
+        // The last 20% has has full compute so blockspace is maximized if BundleStage is idle.
+        let reserved_space =
+            BundleReservedSpaceManager::new(MAX_BLOCK_UNITS, 3_000_000, reserved_ticks);
+
+        assert!(BundleConsumer::reserve_bundle_blockspace(
+            &qos_service,
+            &reserved_space,
+            &sanitized_bundle,
+            &bank
+        )
+        .is_ok());
+        assert_eq!(
+            bank.read_cost_tracker().unwrap().block_cost(),
+            transfer_cost.sum()
+        );
+    }
+
+    #[test]
+    fn test_reserve_bundle_blockspace_failure() {
+        let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10);
+        let bank = Arc::new(Bank::new_for_tests(&genesis_config));
+
+        let keypair1 = Keypair::new();
+        let keypair2 = Keypair::new();
+        let transfer_tx1 = SanitizedTransaction::from_transaction_for_tests(transfer(
+            &keypair1,
+            &keypair2.pubkey(),
+            1,
+            bank.parent_hash(),
+        ));
+        let transfer_tx2 = SanitizedTransaction::from_transaction_for_tests(transfer(
+            &keypair1,
+            &keypair2.pubkey(),
+            2,
+            bank.parent_hash(),
+        ));
+        let sanitized_bundle = SanitizedBundle {
+            transactions: vec![transfer_tx1, transfer_tx2],
+            bundle_id: String::default(),
+        };
+
+        // set block cost limit to 1 transfer transaction, try to process 2, should return an error
+        // and rollback block cost added
+        let transfer_cost =
+            CostModel::calculate_cost(&sanitized_bundle.transactions[0], &bank.feature_set);
+        bank.write_cost_tracker()
+            .unwrap()
+            .set_block_cost_limit(transfer_cost.sum());
+
+        let qos_service = QosService::new(1);
+        let reserved_ticks = bank.max_tick_height().saturating_mul(8).saturating_div(10);
+
+        // The first 80% of the block, based on poh ticks, has `preallocated_bundle_cost` less compute units.
+        // The last 20% has has full compute so blockspace is maximized if BundleStage is idle.
+        let reserved_space = BundleReservedSpaceManager::new(
+            bank.read_cost_tracker().unwrap().block_cost(),
+            50,
+            reserved_ticks,
+        );
+
+        assert!(BundleConsumer::reserve_bundle_blockspace(
+            &qos_service,
+            &reserved_space,
+            &sanitized_bundle,
+            &bank
+        )
+        .is_err());
+        assert_eq!(bank.read_cost_tracker().unwrap().block_cost(), 0);
+        assert_eq!(
+            bank.read_cost_tracker().unwrap().block_cost_limit(),
+            bank.read_cost_tracker()
+                .unwrap()
+                .block_cost_limit()
+                .saturating_sub(50)
+        );
+    }
+}
diff --git a/core/src/bundle_stage/bundle_packet_deserializer.rs b/core/src/bundle_stage/bundle_packet_deserializer.rs
new file mode 100644
index 0000000000..b3af110fc3
--- /dev/null
+++ b/core/src/bundle_stage/bundle_packet_deserializer.rs
@@ -0,0 +1,286 @@
+//! Deserializes PacketBundles
+use {
+    crate::{
+        immutable_deserialized_bundle::{DeserializedBundleError, ImmutableDeserializedBundle},
+        packet_bundle::PacketBundle,
+    },
+    crossbeam_channel::{Receiver, RecvTimeoutError},
+    solana_runtime::bank_forks::BankForks,
+    solana_sdk::saturating_add_assign,
+    std::{
+        sync::{Arc, RwLock},
+        time::{Duration, Instant},
+    },
+};
+
+/// Results from deserializing packet batches.
+#[derive(Debug)]
+pub struct ReceiveBundleResults {
+    /// Deserialized bundles from all received bundle packets
+    pub deserialized_bundles: Vec<ImmutableDeserializedBundle>,
+    /// Number of dropped bundles
+    pub num_dropped_bundles: usize,
+    /// Number of dropped packets
+    pub num_dropped_packets: usize,
+}
+
+pub struct BundlePacketDeserializer {
+    /// Receiver for bundle packets
+    bundle_packet_receiver: Receiver<Vec<PacketBundle>>,
+    /// Provides working bank for deserializer to check feature activation
+    bank_forks: Arc<RwLock<BankForks>>,
+    /// Max packets per bundle
+    max_packets_per_bundle: Option<usize>,
+}
+
+impl BundlePacketDeserializer {
+    pub fn new(
+        bundle_packet_receiver: Receiver<Vec<PacketBundle>>,
+        bank_forks: Arc<RwLock<BankForks>>,
+        max_packets_per_bundle: Option<usize>,
+    ) -> Self {
+        Self {
+            bundle_packet_receiver,
+            bank_forks,
+            max_packets_per_bundle,
+        }
+    }
+
+    /// Handles receiving bundles and deserializing them
+    pub fn receive_bundles(
+        &self,
+        recv_timeout: Duration,
+        capacity: usize,
+    ) -> Result<ReceiveBundleResults, RecvTimeoutError> {
+        let (bundle_count, _packet_count, mut bundles) =
+            self.receive_until(recv_timeout, capacity)?;
+
+        // Note: this can be removed after feature `round_compute_unit_price` is activated in
+        // mainnet-beta
+        let _working_bank = self.bank_forks.read().unwrap().working_bank();
+        let round_compute_unit_price_enabled = false; // TODO get from working_bank.feature_set
+
+        Ok(Self::deserialize_and_collect_bundles(
+            bundle_count,
+            &mut bundles,
+            round_compute_unit_price_enabled,
+            self.max_packets_per_bundle,
+        ))
+    }
+
+    /// Deserialize packet batches, aggregates tracer packet stats, and collect
+    /// them into ReceivePacketResults
+    fn deserialize_and_collect_bundles(
+        bundle_count: usize,
+        bundles: &mut [PacketBundle],
+        round_compute_unit_price_enabled: bool,
+        max_packets_per_bundle: Option<usize>,
+    ) -> ReceiveBundleResults {
+        let mut deserialized_bundles = Vec::with_capacity(bundle_count);
+        let mut num_dropped_bundles: usize = 0;
+        let mut num_dropped_packets: usize = 0;
+
+        for bundle in bundles.iter_mut() {
+            match Self::deserialize_bundle(
+                bundle,
+                round_compute_unit_price_enabled,
+                max_packets_per_bundle,
+            ) {
+                Ok(deserialized_bundle) => {
+                    deserialized_bundles.push(deserialized_bundle);
+                }
+                Err(_) => {
+                    // TODO (LB): prob wanna collect stats here
+                    saturating_add_assign!(num_dropped_bundles, 1);
+                    saturating_add_assign!(num_dropped_packets, bundle.batch.len());
+                }
+            }
+        }
+
+        ReceiveBundleResults {
+            deserialized_bundles,
+            num_dropped_bundles,
+            num_dropped_packets,
+        }
+    }
+
+    /// Receives bundle packets
+    fn receive_until(
+        &self,
+        recv_timeout: Duration,
+        bundle_count_upperbound: usize,
+    ) -> Result<(usize, usize, Vec<PacketBundle>), RecvTimeoutError> {
+        let start = Instant::now();
+
+        let mut bundles = self.bundle_packet_receiver.recv_timeout(recv_timeout)?;
+        let mut num_packets_received: usize = bundles.iter().map(|pb| pb.batch.len()).sum();
+        let mut num_bundles_received: usize = bundles.len();
+
+        if num_bundles_received <= bundle_count_upperbound {
+            while let Ok(bundle_packets) = self.bundle_packet_receiver.try_recv() {
+                trace!("got more packet batches in bundle packet deserializer");
+
+                saturating_add_assign!(
+                    num_packets_received,
+                    bundle_packets
+                        .iter()
+                        .map(|pb| pb.batch.len())
+                        .sum::<usize>()
+                );
+                saturating_add_assign!(num_bundles_received, bundle_packets.len());
+
+                bundles.extend(bundle_packets);
+
+                if start.elapsed() >= recv_timeout
+                    || num_bundles_received >= bundle_count_upperbound
+                {
+                    break;
+                }
+            }
+        }
+
+        Ok((num_bundles_received, num_packets_received, bundles))
+    }
+
+    /// Deserializes the Bundle into DeserializedBundlePackets, returning None if any packet in the
+    /// bundle failed to deserialize
+    pub fn deserialize_bundle(
+        bundle: &mut PacketBundle,
+        round_compute_unit_price_enabled: bool,
+        max_packets_per_bundle: Option<usize>,
+    ) -> Result<ImmutableDeserializedBundle, DeserializedBundleError> {
+        bundle.batch.iter_mut().for_each(|p| {
+            p.meta_mut()
+                .set_round_compute_unit_price(round_compute_unit_price_enabled);
+        });
+
+        ImmutableDeserializedBundle::new(bundle, max_packets_per_bundle)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {
+        super::*,
+        crossbeam_channel::unbounded,
+        solana_ledger::genesis_utils::create_genesis_config,
+        solana_perf::packet::PacketBatch,
+        solana_runtime::{bank::Bank, genesis_utils::GenesisConfigInfo},
+        solana_sdk::{packet::Packet, signature::Signer, system_transaction::transfer},
+    };
+
+    #[test]
+    fn test_deserialize_and_collect_bundles_empty() {
+        let results =
+            BundlePacketDeserializer::deserialize_and_collect_bundles(0, &mut [], false, Some(5));
+        assert_eq!(results.deserialized_bundles.len(), 0);
+        assert_eq!(results.num_dropped_packets, 0);
+        assert_eq!(results.num_dropped_bundles, 0);
+    }
+
+    #[test]
+    fn test_receive_bundles_capacity() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let (sender, receiver) = unbounded();
+
+        let deserializer = BundlePacketDeserializer::new(receiver, bank_forks, Some(10));
+
+        let packet_bundles: Vec<_> = (0..10)
+            .map(|_| PacketBundle {
+                batch: PacketBatch::new(vec![Packet::from_data(
+                    None,
+                    transfer(
+                        &mint_keypair,
+                        &mint_keypair.pubkey(),
+                        100,
+                        genesis_config.hash(),
+                    ),
+                )
+                .unwrap()]),
+                bundle_id: String::default(),
+            })
+            .collect();
+
+        sender.send(packet_bundles.clone()).unwrap();
+
+        let bundles = deserializer
+            .receive_bundles(Duration::from_millis(100), 5)
+            .unwrap();
+        // this is confusing, but it's sent as one batch
+        assert_eq!(bundles.deserialized_bundles.len(), 10);
+        assert_eq!(bundles.num_dropped_bundles, 0);
+        assert_eq!(bundles.num_dropped_packets, 0);
+
+        // make sure empty
+        assert_matches!(
+            deserializer.receive_bundles(Duration::from_millis(100), 5),
+            Err(RecvTimeoutError::Timeout)
+        );
+
+        // send 2x 10 size batches. capacity is 5, but will return 10 since that's the batch size
+        sender.send(packet_bundles.clone()).unwrap();
+        sender.send(packet_bundles).unwrap();
+        let bundles = deserializer
+            .receive_bundles(Duration::from_millis(100), 5)
+            .unwrap();
+        assert_eq!(bundles.deserialized_bundles.len(), 10);
+        assert_eq!(bundles.num_dropped_bundles, 0);
+        assert_eq!(bundles.num_dropped_packets, 0);
+
+        let bundles = deserializer
+            .receive_bundles(Duration::from_millis(100), 5)
+            .unwrap();
+        assert_eq!(bundles.deserialized_bundles.len(), 10);
+        assert_eq!(bundles.num_dropped_bundles, 0);
+        assert_eq!(bundles.num_dropped_packets, 0);
+
+        assert_matches!(
+            deserializer.receive_bundles(Duration::from_millis(100), 5),
+            Err(RecvTimeoutError::Timeout)
+        );
+    }
+
+    #[test]
+    fn test_receive_bundles_bad_bundles() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair: _,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let (sender, receiver) = unbounded();
+
+        let deserializer = BundlePacketDeserializer::new(receiver, bank_forks, Some(10));
+
+        let packet_bundles: Vec<_> = (0..10)
+            .map(|_| PacketBundle {
+                batch: PacketBatch::new(vec![]),
+                bundle_id: String::default(),
+            })
+            .collect();
+        sender.send(packet_bundles).unwrap();
+
+        let bundles = deserializer
+            .receive_bundles(Duration::from_millis(100), 5)
+            .unwrap();
+        // this is confusing, but it's sent as one batch
+        assert_eq!(bundles.deserialized_bundles.len(), 0);
+        assert_eq!(bundles.num_dropped_bundles, 10);
+        assert_eq!(bundles.num_dropped_packets, 0);
+    }
+}
diff --git a/core/src/bundle_stage/bundle_packet_receiver.rs b/core/src/bundle_stage/bundle_packet_receiver.rs
new file mode 100644
index 0000000000..ffdf47c7f7
--- /dev/null
+++ b/core/src/bundle_stage/bundle_packet_receiver.rs
@@ -0,0 +1,848 @@
+use {
+    super::BundleStageLoopMetrics,
+    crate::{
+        bundle_stage::{
+            bundle_packet_deserializer::{BundlePacketDeserializer, ReceiveBundleResults},
+            bundle_stage_leader_metrics::BundleStageLeaderMetrics,
+        },
+        immutable_deserialized_bundle::ImmutableDeserializedBundle,
+        packet_bundle::PacketBundle,
+        unprocessed_transaction_storage::UnprocessedTransactionStorage,
+    },
+    crossbeam_channel::{Receiver, RecvTimeoutError},
+    solana_measure::{measure::Measure, measure_us},
+    solana_runtime::bank_forks::BankForks,
+    solana_sdk::timing::timestamp,
+    std::{
+        sync::{Arc, RwLock},
+        time::Duration,
+    },
+};
+
+pub struct BundleReceiver {
+    id: u32,
+    bundle_packet_deserializer: BundlePacketDeserializer,
+}
+
+impl BundleReceiver {
+    pub fn new(
+        id: u32,
+        bundle_packet_receiver: Receiver<Vec<PacketBundle>>,
+        bank_forks: Arc<RwLock<BankForks>>,
+        max_packets_per_bundle: Option<usize>,
+    ) -> Self {
+        Self {
+            id,
+            bundle_packet_deserializer: BundlePacketDeserializer::new(
+                bundle_packet_receiver,
+                bank_forks,
+                max_packets_per_bundle,
+            ),
+        }
+    }
+
+    /// Receive incoming packets, push into unprocessed buffer with packet indexes
+    pub fn receive_and_buffer_bundles(
+        &mut self,
+        unprocessed_bundle_storage: &mut UnprocessedTransactionStorage,
+        bundle_stage_metrics: &mut BundleStageLoopMetrics,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+    ) -> Result<(), RecvTimeoutError> {
+        let (result, recv_time_us) = measure_us!({
+            let recv_timeout = Self::get_receive_timeout(unprocessed_bundle_storage);
+            let mut recv_and_buffer_measure = Measure::start("recv_and_buffer");
+            self.bundle_packet_deserializer
+                .receive_bundles(recv_timeout, unprocessed_bundle_storage.max_receive_size())
+                // Consumes results if Ok, otherwise we keep the Err
+                .map(|receive_bundle_results| {
+                    self.buffer_bundles(
+                        receive_bundle_results,
+                        unprocessed_bundle_storage,
+                        bundle_stage_metrics,
+                        // tracer_packet_stats,
+                        bundle_stage_leader_metrics,
+                    );
+                    recv_and_buffer_measure.stop();
+                    bundle_stage_metrics.increment_receive_and_buffer_bundles_elapsed_us(
+                        recv_and_buffer_measure.as_us(),
+                    );
+                })
+        });
+
+        bundle_stage_leader_metrics
+            .leader_slot_metrics_tracker()
+            .increment_receive_and_buffer_packets_us(recv_time_us);
+
+        result
+    }
+
+    fn get_receive_timeout(
+        unprocessed_transaction_storage: &UnprocessedTransactionStorage,
+    ) -> Duration {
+        // Gossip thread will almost always not wait because the transaction storage will most likely not be empty
+        if !unprocessed_transaction_storage.is_empty() {
+            // If there are buffered packets, run the equivalent of try_recv to try reading more
+            // packets. This prevents starving BankingStage::consume_buffered_packets due to
+            // buffered_packet_batches containing transactions that exceed the cost model for
+            // the current bank.
+            Duration::from_millis(0)
+        } else {
+            // BundleStage should pick up a working_bank as fast as possible
+            Duration::from_millis(100)
+        }
+    }
+
+    fn buffer_bundles(
+        &self,
+        ReceiveBundleResults {
+            deserialized_bundles,
+            num_dropped_bundles: _,
+            num_dropped_packets: _,
+        }: ReceiveBundleResults,
+        unprocessed_transaction_storage: &mut UnprocessedTransactionStorage,
+        bundle_stage_stats: &mut BundleStageLoopMetrics,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+    ) {
+        let bundle_count = deserialized_bundles.len();
+        let packet_count: usize = deserialized_bundles.iter().map(|b| b.len()).sum();
+
+        bundle_stage_stats.increment_num_bundles_received(bundle_count as u64);
+        bundle_stage_stats.increment_num_packets_received(packet_count as u64);
+        bundle_stage_leader_metrics
+            .leader_slot_metrics_tracker()
+            .increment_total_new_valid_packets(packet_count as u64);
+
+        debug!(
+            "@{:?} bundles: {} txs: {} id: {}",
+            timestamp(),
+            bundle_count,
+            packet_count,
+            self.id
+        );
+
+        Self::push_unprocessed(
+            unprocessed_transaction_storage,
+            deserialized_bundles,
+            bundle_stage_leader_metrics,
+            bundle_stage_stats,
+        );
+    }
+
+    fn push_unprocessed(
+        unprocessed_transaction_storage: &mut UnprocessedTransactionStorage,
+        deserialized_bundles: Vec<ImmutableDeserializedBundle>,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+        bundle_stage_stats: &mut BundleStageLoopMetrics,
+    ) {
+        if !deserialized_bundles.is_empty() {
+            let insert_bundles_summary =
+                unprocessed_transaction_storage.insert_bundles(deserialized_bundles);
+
+            bundle_stage_stats.increment_newly_buffered_bundles_count(
+                insert_bundles_summary.num_bundles_inserted as u64,
+            );
+            bundle_stage_stats
+                .increment_num_bundles_dropped(insert_bundles_summary.num_bundles_dropped as u64);
+
+            bundle_stage_leader_metrics
+                .leader_slot_metrics_tracker()
+                .increment_newly_buffered_packets_count(
+                    insert_bundles_summary.num_packets_inserted as u64,
+                );
+
+            bundle_stage_leader_metrics
+                .leader_slot_metrics_tracker()
+                .accumulate_insert_packet_batches_summary(
+                    &insert_bundles_summary.insert_packets_summary,
+                );
+        }
+    }
+}
+
+/// This tests functionality of BundlePacketReceiver and the internals of BundleStorage because
+/// they're tightly intertwined
+#[cfg(test)]
+mod tests {
+    use {
+        super::*,
+        crossbeam_channel::unbounded,
+        rand::{thread_rng, RngCore},
+        solana_bundle::{
+            bundle_execution::LoadAndExecuteBundleError, BundleExecutionError, TipError,
+        },
+        solana_ledger::genesis_utils::create_genesis_config,
+        solana_perf::packet::PacketBatch,
+        solana_poh::poh_recorder::PohRecorderError,
+        solana_runtime::{bank::Bank, genesis_utils::GenesisConfigInfo},
+        solana_sdk::{
+            bundle::{derive_bundle_id, SanitizedBundle},
+            hash::Hash,
+            packet::Packet,
+            signature::{Keypair, Signer},
+            system_transaction::transfer,
+            transaction::VersionedTransaction,
+        },
+        std::collections::{HashSet, VecDeque},
+    };
+
+    /// Makes `num_bundles` random bundles with `num_packets_per_bundle` packets per bundle.
+    fn make_random_bundles(
+        mint_keypair: &Keypair,
+        num_bundles: usize,
+        num_packets_per_bundle: usize,
+        hash: Hash,
+    ) -> Vec<PacketBundle> {
+        let mut rng = thread_rng();
+
+        (0..num_bundles)
+            .map(|_| {
+                let transfers: Vec<_> = (0..num_packets_per_bundle)
+                    .map(|_| {
+                        VersionedTransaction::from(transfer(
+                            mint_keypair,
+                            &mint_keypair.pubkey(),
+                            rng.next_u64(),
+                            hash,
+                        ))
+                    })
+                    .collect();
+                let bundle_id = derive_bundle_id(&transfers);
+
+                PacketBundle {
+                    batch: PacketBatch::new(
+                        transfers
+                            .iter()
+                            .map(|tx| Packet::from_data(None, tx).unwrap())
+                            .collect(),
+                    ),
+                    bundle_id,
+                }
+            })
+            .collect()
+    }
+
+    fn assert_bundles_same(
+        packet_bundles: &[PacketBundle],
+        bundles_to_process: &[(ImmutableDeserializedBundle, SanitizedBundle)],
+    ) {
+        assert_eq!(packet_bundles.len(), bundles_to_process.len());
+        packet_bundles
+            .iter()
+            .zip(bundles_to_process.iter())
+            .for_each(|(packet_bundle, (_, sanitized_bundle))| {
+                assert_eq!(packet_bundle.bundle_id, sanitized_bundle.bundle_id);
+                assert_eq!(
+                    packet_bundle.batch.len(),
+                    sanitized_bundle.transactions.len()
+                );
+            });
+    }
+
+    #[test]
+    fn test_receive_bundles() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let mut unprocessed_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(1_000),
+            VecDeque::with_capacity(1_000),
+        );
+
+        let (sender, receiver) = unbounded();
+        let mut bundle_receiver = BundleReceiver::new(0, receiver, bank_forks.clone(), Some(5));
+
+        let bundles = make_random_bundles(&mint_keypair, 10, 2, genesis_config.hash());
+        sender.send(bundles.clone()).unwrap();
+
+        let mut bundle_stage_stats = BundleStageLoopMetrics::default();
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(0);
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 10);
+        assert_eq!(bundle_storage.unprocessed_packets_len(), 20);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_packets_len(), 0);
+        assert_eq!(bundle_storage.max_receive_size(), 990);
+
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles, bundles_to_process);
+                (0..bundles_to_process.len()).map(|_| Ok(())).collect()
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+        assert_eq!(bundle_storage.unprocessed_packets_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_packets_len(), 0);
+        assert_eq!(bundle_storage.max_receive_size(), 1000);
+    }
+
+    #[test]
+    fn test_receive_more_bundles_than_capacity() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let mut unprocessed_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(10),
+            VecDeque::with_capacity(10),
+        );
+
+        let (sender, receiver) = unbounded();
+        let mut bundle_receiver = BundleReceiver::new(0, receiver, bank_forks.clone(), Some(5));
+
+        let bundles = make_random_bundles(&mint_keypair, 15, 2, genesis_config.hash());
+
+        sender.send(bundles.clone()).unwrap();
+
+        let mut bundle_stage_stats = BundleStageLoopMetrics::default();
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(0);
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+        // 15 bundles were sent, but the capacity is 10
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 10);
+        assert_eq!(bundle_storage.unprocessed_packets_len(), 20);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_packets_len(), 0);
+
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                // make sure the first 10 bundles are the ones to process
+                assert_bundles_same(&bundles[0..10], bundles_to_process);
+                (0..bundles_to_process.len()).map(|_| Ok(())).collect()
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 0);
+    }
+
+    #[test]
+    fn test_process_bundles_poh_record_error_rebuffered() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let mut unprocessed_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(10),
+            VecDeque::with_capacity(10),
+        );
+
+        let (sender, receiver) = unbounded();
+        let mut bundle_receiver = BundleReceiver::new(0, receiver, bank_forks.clone(), Some(5));
+
+        // send 5 bundles across the queue
+        let bundles = make_random_bundles(&mint_keypair, 5, 2, genesis_config.hash());
+        sender.send(bundles.clone()).unwrap();
+
+        let mut bundle_stage_stats = BundleStageLoopMetrics::default();
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(0);
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let poh_max_height_reached_index = 3;
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+
+        // make sure poh end of slot reached + the correct bundles are buffered for the next time.
+        // bundles at index 3 + 4 are rebuffered
+        assert!(bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles, bundles_to_process);
+
+                let mut results = vec![Ok(()); bundles_to_process.len()];
+
+                (poh_max_height_reached_index..bundles_to_process.len()).for_each(|index| {
+                    results[index] = Err(BundleExecutionError::PohRecordError(
+                        PohRecorderError::MaxHeightReached,
+                    ));
+                });
+                results
+            }
+        ));
+
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 2);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 0);
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles[poh_max_height_reached_index..], bundles_to_process);
+                vec![Ok(()); bundles_to_process.len()]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+    }
+
+    #[test]
+    fn test_process_bundles_bank_processing_done_rebuffered() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let mut unprocessed_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(10),
+            VecDeque::with_capacity(10),
+        );
+
+        let (sender, receiver) = unbounded();
+        let mut bundle_receiver = BundleReceiver::new(0, receiver, bank_forks.clone(), Some(5));
+
+        // send 5 bundles across the queue
+        let bundles = make_random_bundles(&mint_keypair, 5, 2, genesis_config.hash());
+        sender.send(bundles.clone()).unwrap();
+
+        let mut bundle_stage_stats = BundleStageLoopMetrics::default();
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(0);
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let bank_processing_done_index = 3;
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+
+        // bundles at index 3 + 4 are rebuffered
+        assert!(bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles, bundles_to_process);
+
+                let mut results = vec![Ok(()); bundles_to_process.len()];
+
+                (bank_processing_done_index..bundles_to_process.len()).for_each(|index| {
+                    results[index] = Err(BundleExecutionError::BankProcessingTimeLimitReached);
+                });
+                results
+            }
+        ));
+
+        // 0, 1, 2 processed; 3, 4 buffered
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 2);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 0);
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles[bank_processing_done_index..], bundles_to_process);
+                vec![Ok(()); bundles_to_process.len()]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+    }
+
+    #[test]
+    fn test_process_bundles_bank_execution_error_dropped() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let mut unprocessed_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(10),
+            VecDeque::with_capacity(10),
+        );
+
+        let (sender, receiver) = unbounded();
+        let mut bundle_receiver = BundleReceiver::new(0, receiver, bank_forks.clone(), Some(5));
+
+        // send 5 bundles across the queue
+        let bundles = make_random_bundles(&mint_keypair, 5, 2, genesis_config.hash());
+        sender.send(bundles.clone()).unwrap();
+
+        let mut bundle_stage_stats = BundleStageLoopMetrics::default();
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(0);
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles, bundles_to_process);
+                vec![
+                    Err(BundleExecutionError::TransactionFailure(
+                        LoadAndExecuteBundleError::ProcessingTimeExceeded(Duration::from_secs(1)),
+                    ));
+                    bundles_to_process.len()
+                ]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+    }
+
+    #[test]
+    fn test_process_bundles_tip_error_dropped() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let mut unprocessed_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(10),
+            VecDeque::with_capacity(10),
+        );
+
+        let (sender, receiver) = unbounded();
+        let mut bundle_receiver = BundleReceiver::new(0, receiver, bank_forks.clone(), Some(5));
+
+        // send 5 bundles across the queue
+        let bundles = make_random_bundles(&mint_keypair, 5, 2, genesis_config.hash());
+        sender.send(bundles.clone()).unwrap();
+
+        let mut bundle_stage_stats = BundleStageLoopMetrics::default();
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(0);
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles, bundles_to_process);
+                vec![
+                    Err(BundleExecutionError::TipError(TipError::LockError));
+                    bundles_to_process.len()
+                ]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+    }
+
+    #[test]
+    fn test_process_bundles_lock_error_dropped() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let mut unprocessed_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(10),
+            VecDeque::with_capacity(10),
+        );
+
+        let (sender, receiver) = unbounded();
+        let mut bundle_receiver = BundleReceiver::new(0, receiver, bank_forks.clone(), Some(5));
+
+        // send 5 bundles across the queue
+        let bundles = make_random_bundles(&mint_keypair, 5, 2, genesis_config.hash());
+        sender.send(bundles).unwrap();
+
+        let mut bundle_stage_stats = BundleStageLoopMetrics::default();
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(0);
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                vec![Err(BundleExecutionError::LockError); bundles_to_process.len()]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+    }
+
+    #[test]
+    fn test_process_bundles_cost_model_exceeded_set_aside_and_requeued() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let mut unprocessed_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(10),
+            VecDeque::with_capacity(10),
+        );
+
+        let (sender, receiver) = unbounded();
+        let mut bundle_receiver = BundleReceiver::new(0, receiver, bank_forks.clone(), Some(5));
+
+        // send 5 bundles across the queue
+        let bundles = make_random_bundles(&mint_keypair, 5, 2, genesis_config.hash());
+        sender.send(bundles.clone()).unwrap();
+
+        let mut bundle_stage_stats = BundleStageLoopMetrics::default();
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(0);
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+
+        // buffered bundles are moved to cost model side deque
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles, bundles_to_process);
+                vec![Err(BundleExecutionError::ExceedsCostModel); bundles_to_process.len()]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 5);
+
+        // double check there's no bundles to process
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert!(bundles_to_process.is_empty());
+                vec![Ok(()); bundles_to_process.len()]
+            }
+        ));
+
+        // create a new bank w/ new slot number, cost model buffered packets should move back onto queue
+        // in the same order they were originally
+        let bank = &bank_forks.read().unwrap().working_bank();
+        let new_bank = Arc::new(Bank::new_from_parent(
+            bank,
+            bank.collector_id(),
+            bank.slot() + 1,
+        ));
+        assert!(!bundle_storage.process_bundles(
+            new_bank,
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                // make sure same order as original
+                assert_bundles_same(&bundles, bundles_to_process);
+                vec![Ok(()); bundles_to_process.len()]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 0);
+    }
+
+    #[test]
+    fn test_process_bundles_cost_model_exceeded_buffer_capacity() {
+        solana_logger::setup();
+
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank_forks = Arc::new(RwLock::new(BankForks::new(
+            Bank::new_no_wallclock_throttle_for_tests(&genesis_config),
+        )));
+
+        let mut unprocessed_storage = UnprocessedTransactionStorage::new_bundle_storage(
+            VecDeque::with_capacity(10),
+            VecDeque::with_capacity(10),
+        );
+
+        let (sender, receiver) = unbounded();
+        let mut bundle_receiver = BundleReceiver::new(0, receiver, bank_forks.clone(), Some(5));
+
+        // send 15 bundles across the queue
+        let bundles0 = make_random_bundles(&mint_keypair, 5, 2, genesis_config.hash());
+        sender.send(bundles0.clone()).unwrap();
+
+        let mut bundle_stage_stats = BundleStageLoopMetrics::default();
+        let mut bundle_stage_leader_metrics = BundleStageLeaderMetrics::new(0);
+
+        // receive and buffer bundles to the cost model reserve to test the capacity/dropped bundles there
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+        // buffered bundles are moved to cost model side deque
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles0, bundles_to_process);
+                vec![Err(BundleExecutionError::ExceedsCostModel); bundles_to_process.len()]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 5);
+
+        let bundles1 = make_random_bundles(&mint_keypair, 5, 2, genesis_config.hash());
+        sender.send(bundles1.clone()).unwrap();
+        // should get 5 more bundles + cost model buffered length should be 10
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+        // buffered bundles are moved to cost model side deque
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles1, bundles_to_process);
+                vec![Err(BundleExecutionError::ExceedsCostModel); bundles_to_process.len()]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 10);
+
+        let bundles2 = make_random_bundles(&mint_keypair, 5, 2, genesis_config.hash());
+        sender.send(bundles2.clone()).unwrap();
+
+        // this set will get dropped from cost model buffered bundles
+        let result = bundle_receiver.receive_and_buffer_bundles(
+            &mut unprocessed_storage,
+            &mut bundle_stage_stats,
+            &mut bundle_stage_leader_metrics,
+        );
+        assert!(result.is_ok());
+
+        let bundle_storage = unprocessed_storage.bundle_storage().unwrap();
+        // buffered bundles are moved to cost model side deque, but its at capacity so stays the same size
+        assert!(!bundle_storage.process_bundles(
+            bank_forks.read().unwrap().working_bank(),
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                assert_bundles_same(&bundles2, bundles_to_process);
+                vec![Err(BundleExecutionError::ExceedsCostModel); bundles_to_process.len()]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 10);
+
+        // create new bank then call process_bundles again, expect to see [bundles1,bundles2]
+        let bank = &bank_forks.read().unwrap().working_bank();
+        let new_bank = Arc::new(Bank::new_from_parent(
+            bank,
+            bank.collector_id(),
+            bank.slot() + 1,
+        ));
+        assert!(!bundle_storage.process_bundles(
+            new_bank,
+            &mut bundle_stage_leader_metrics,
+            &HashSet::default(),
+            |bundles_to_process, _stats| {
+                // make sure same order as original
+                let expected_bundles: Vec<_> =
+                    bundles0.iter().chain(bundles1.iter()).cloned().collect();
+                assert_bundles_same(&expected_bundles, bundles_to_process);
+                vec![Ok(()); bundles_to_process.len()]
+            }
+        ));
+        assert_eq!(bundle_storage.unprocessed_bundles_len(), 0);
+        assert_eq!(bundle_storage.cost_model_buffered_bundles_len(), 0);
+    }
+}
diff --git a/core/src/bundle_stage/bundle_reserved_space_manager.rs b/core/src/bundle_stage/bundle_reserved_space_manager.rs
new file mode 100644
index 0000000000..42cb7adeb6
--- /dev/null
+++ b/core/src/bundle_stage/bundle_reserved_space_manager.rs
@@ -0,0 +1,189 @@
+use {solana_runtime::bank::Bank, solana_sdk::clock::Slot, std::sync::Arc};
+
+/// Manager responsible for reserving `bundle_reserved_cost` during the first `reserved_ticks` of a bank
+/// and resetting the block cost limit to `block_cost_limit` after the reserved tick period is over
+pub struct BundleReservedSpaceManager {
+    // the bank's cost limit
+    block_cost_limit: u64,
+    // bundles get this much reserved space for the first reserved_ticks
+    bundle_reserved_cost: u64,
+    // a reduced block_compute_limit is reserved for this many ticks, afterwards it goes back to full cost
+    reserved_ticks: u64,
+    last_slot_updated: Slot,
+}
+
+impl BundleReservedSpaceManager {
+    pub fn new(block_cost_limit: u64, bundle_reserved_cost: u64, reserved_ticks: u64) -> Self {
+        Self {
+            block_cost_limit,
+            bundle_reserved_cost,
+            reserved_ticks,
+            last_slot_updated: u64::MAX,
+        }
+    }
+
+    /// Call this on creation of new bank and periodically while bundle processing
+    /// to manage the block_cost_limits
+    pub fn tick(&mut self, bank: &Arc<Bank>) {
+        if self.last_slot_updated == bank.slot() && !self.is_in_reserved_tick_period(bank) {
+            // new slot logic already ran, need to revert the block cost limit to original if
+            // ticks are past the reserved tick mark
+            debug!(
+                "slot: {} ticks: {}, resetting block_cost_limit to {}",
+                bank.slot(),
+                bank.tick_height(),
+                self.block_cost_limit
+            );
+            bank.write_cost_tracker()
+                .unwrap()
+                .set_block_cost_limit(self.block_cost_limit);
+        } else if self.last_slot_updated != bank.slot() && self.is_in_reserved_tick_period(bank) {
+            // new slot, if in the first max_tick - tick_height slots reserve space
+            // otherwise can leave the current block limit as is
+            let new_block_cost_limit = self.reduced_block_cost_limit();
+            debug!(
+                "slot: {} ticks: {}, reserving block_cost_limit with block_cost_limit of {}",
+                bank.slot(),
+                bank.tick_height(),
+                new_block_cost_limit
+            );
+            bank.write_cost_tracker()
+                .unwrap()
+                .set_block_cost_limit(new_block_cost_limit);
+            self.last_slot_updated = bank.slot();
+        }
+    }
+
+    /// return true if the bank is still in the period where block_cost_limits is reduced
+    pub fn is_in_reserved_tick_period(&self, bank: &Bank) -> bool {
+        bank.tick_height() < self.reserved_ticks
+    }
+
+    /// return the block_cost_limits as determined by the tick height of the bank
+    pub fn expected_block_cost_limits(&self, bank: &Bank) -> u64 {
+        if self.is_in_reserved_tick_period(bank) {
+            self.reduced_block_cost_limit()
+        } else {
+            self.block_cost_limit()
+        }
+    }
+
+    pub fn reduced_block_cost_limit(&self) -> u64 {
+        self.block_cost_limit
+            .saturating_sub(self.bundle_reserved_cost)
+    }
+
+    pub fn block_cost_limit(&self) -> u64 {
+        self.block_cost_limit
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {
+        crate::bundle_stage::bundle_reserved_space_manager::BundleReservedSpaceManager,
+        solana_ledger::genesis_utils::create_genesis_config, solana_runtime::bank::Bank,
+        solana_sdk::hash::Hash, std::sync::Arc,
+    };
+
+    #[test]
+    fn test_reserve_block_cost_limits_during_reserved_ticks() {
+        const BUNDLE_BLOCK_COST_LIMITS_RESERVATION: u64 = 100;
+
+        let genesis_config_info = create_genesis_config(100);
+        let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
+
+        let block_cost_limits = bank.read_cost_tracker().unwrap().block_cost_limit();
+
+        let mut reserved_space = BundleReservedSpaceManager::new(
+            block_cost_limits,
+            BUNDLE_BLOCK_COST_LIMITS_RESERVATION,
+            5,
+        );
+        reserved_space.tick(&bank);
+
+        assert_eq!(
+            bank.read_cost_tracker().unwrap().block_cost_limit(),
+            block_cost_limits - BUNDLE_BLOCK_COST_LIMITS_RESERVATION
+        );
+    }
+
+    #[test]
+    fn test_dont_reserve_block_cost_limits_after_reserved_ticks() {
+        const BUNDLE_BLOCK_COST_LIMITS_RESERVATION: u64 = 100;
+
+        let genesis_config_info = create_genesis_config(100);
+        let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
+
+        let block_cost_limits = bank.read_cost_tracker().unwrap().block_cost_limit();
+
+        for _ in 0..5 {
+            bank.register_tick(&Hash::default());
+        }
+
+        let mut reserved_space = BundleReservedSpaceManager::new(
+            block_cost_limits,
+            BUNDLE_BLOCK_COST_LIMITS_RESERVATION,
+            5,
+        );
+        reserved_space.tick(&bank);
+
+        assert_eq!(
+            bank.read_cost_tracker().unwrap().block_cost_limit(),
+            block_cost_limits
+        );
+    }
+
+    #[test]
+    fn test_dont_reset_block_cost_limits_during_reserved_ticks() {
+        const BUNDLE_BLOCK_COST_LIMITS_RESERVATION: u64 = 100;
+
+        let genesis_config_info = create_genesis_config(100);
+        let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
+
+        let block_cost_limits = bank.read_cost_tracker().unwrap().block_cost_limit();
+
+        let mut reserved_space = BundleReservedSpaceManager::new(
+            block_cost_limits,
+            BUNDLE_BLOCK_COST_LIMITS_RESERVATION,
+            5,
+        );
+
+        reserved_space.tick(&bank);
+        bank.register_tick(&Hash::default());
+        reserved_space.tick(&bank);
+
+        assert_eq!(
+            bank.read_cost_tracker().unwrap().block_cost_limit(),
+            block_cost_limits - BUNDLE_BLOCK_COST_LIMITS_RESERVATION
+        );
+    }
+
+    #[test]
+    fn test_reset_block_cost_limits_after_reserved_ticks() {
+        const BUNDLE_BLOCK_COST_LIMITS_RESERVATION: u64 = 100;
+
+        let genesis_config_info = create_genesis_config(100);
+        let bank = Arc::new(Bank::new_for_tests(&genesis_config_info.genesis_config));
+
+        let block_cost_limits = bank.read_cost_tracker().unwrap().block_cost_limit();
+
+        let mut reserved_space = BundleReservedSpaceManager::new(
+            block_cost_limits,
+            BUNDLE_BLOCK_COST_LIMITS_RESERVATION,
+            5,
+        );
+
+        reserved_space.tick(&bank);
+
+        for _ in 0..5 {
+            bank.register_tick(&Hash::default());
+        }
+        reserved_space.tick(&bank);
+
+        assert_eq!(
+            bank.read_cost_tracker().unwrap().block_cost_limit(),
+            block_cost_limits
+        );
+    }
+}
diff --git a/core/src/bundle_stage/bundle_stage_leader_metrics.rs b/core/src/bundle_stage/bundle_stage_leader_metrics.rs
new file mode 100644
index 0000000000..a32e874bb3
--- /dev/null
+++ b/core/src/bundle_stage/bundle_stage_leader_metrics.rs
@@ -0,0 +1,502 @@
+use {
+    crate::{
+        immutable_deserialized_bundle::DeserializedBundleError,
+        leader_slot_banking_stage_metrics::{self, LeaderSlotMetricsTracker},
+    },
+    solana_bundle::{bundle_execution::LoadAndExecuteBundleError, BundleExecutionError},
+    solana_poh::poh_recorder::BankStart,
+    solana_sdk::{bundle::SanitizedBundle, clock::Slot, saturating_add_assign},
+};
+
+pub struct BundleStageLeaderMetrics {
+    bundle_stage_metrics_tracker: BundleStageStatsMetricsTracker,
+    leader_slot_metrics_tracker: LeaderSlotMetricsTracker,
+}
+
+pub(crate) enum MetricsTrackerAction {
+    Noop,
+    ReportAndResetTracker,
+    NewTracker(Option<BundleStageStats>),
+    ReportAndNewTracker(Option<BundleStageStats>),
+}
+
+impl BundleStageLeaderMetrics {
+    pub fn new(id: u32) -> Self {
+        Self {
+            bundle_stage_metrics_tracker: BundleStageStatsMetricsTracker::new(id),
+            leader_slot_metrics_tracker: LeaderSlotMetricsTracker::new(id),
+        }
+    }
+
+    pub(crate) fn check_leader_slot_boundary(
+        &mut self,
+        bank_start: Option<&BankStart>,
+    ) -> (
+        leader_slot_banking_stage_metrics::MetricsTrackerAction,
+        MetricsTrackerAction,
+    ) {
+        let banking_stage_metrics_action = self
+            .leader_slot_metrics_tracker
+            .check_leader_slot_boundary(bank_start);
+        let bundle_stage_metrics_action = self
+            .bundle_stage_metrics_tracker
+            .check_leader_slot_boundary(bank_start);
+        (banking_stage_metrics_action, bundle_stage_metrics_action)
+    }
+
+    pub(crate) fn apply_action(
+        &mut self,
+        banking_stage_metrics_action: leader_slot_banking_stage_metrics::MetricsTrackerAction,
+        bundle_stage_metrics_action: MetricsTrackerAction,
+    ) -> Option<Slot> {
+        self.leader_slot_metrics_tracker
+            .apply_action(banking_stage_metrics_action);
+        self.bundle_stage_metrics_tracker
+            .apply_action(bundle_stage_metrics_action)
+    }
+
+    pub fn leader_slot_metrics_tracker(&mut self) -> &mut LeaderSlotMetricsTracker {
+        &mut self.leader_slot_metrics_tracker
+    }
+
+    pub fn bundle_stage_metrics_tracker(&mut self) -> &mut BundleStageStatsMetricsTracker {
+        &mut self.bundle_stage_metrics_tracker
+    }
+}
+
+pub struct BundleStageStatsMetricsTracker {
+    bundle_stage_metrics: Option<BundleStageStats>,
+    id: u32,
+}
+
+impl BundleStageStatsMetricsTracker {
+    pub fn new(id: u32) -> Self {
+        Self {
+            bundle_stage_metrics: None,
+            id,
+        }
+    }
+
+    /// Similar to as LeaderSlotMetricsTracker::check_leader_slot_boundary
+    pub(crate) fn check_leader_slot_boundary(
+        &mut self,
+        bank_start: Option<&BankStart>,
+    ) -> MetricsTrackerAction {
+        match (self.bundle_stage_metrics.as_mut(), bank_start) {
+            (None, None) => MetricsTrackerAction::Noop,
+            (Some(_), None) => MetricsTrackerAction::ReportAndResetTracker,
+            // Our leader slot has begun, time to create a new slot tracker
+            (None, Some(bank_start)) => MetricsTrackerAction::NewTracker(Some(
+                BundleStageStats::new(self.id, bank_start.working_bank.slot()),
+            )),
+            (Some(bundle_stage_metrics), Some(bank_start)) => {
+                if bundle_stage_metrics.slot != bank_start.working_bank.slot() {
+                    // Last slot has ended, new slot has began
+                    MetricsTrackerAction::ReportAndNewTracker(Some(BundleStageStats::new(
+                        self.id,
+                        bank_start.working_bank.slot(),
+                    )))
+                } else {
+                    MetricsTrackerAction::Noop
+                }
+            }
+        }
+    }
+
+    /// Similar to LeaderSlotMetricsTracker::apply_action
+    pub(crate) fn apply_action(&mut self, action: MetricsTrackerAction) -> Option<Slot> {
+        match action {
+            MetricsTrackerAction::Noop => None,
+            MetricsTrackerAction::ReportAndResetTracker => {
+                let mut reported_slot = None;
+                if let Some(bundle_stage_metrics) = self.bundle_stage_metrics.as_mut() {
+                    bundle_stage_metrics.report();
+                    reported_slot = bundle_stage_metrics.reported_slot();
+                }
+                self.bundle_stage_metrics = None;
+                reported_slot
+            }
+            MetricsTrackerAction::NewTracker(new_bundle_stage_metrics) => {
+                self.bundle_stage_metrics = new_bundle_stage_metrics;
+                self.bundle_stage_metrics.as_ref().unwrap().reported_slot()
+            }
+            MetricsTrackerAction::ReportAndNewTracker(new_bundle_stage_metrics) => {
+                let mut reported_slot = None;
+                if let Some(bundle_stage_metrics) = self.bundle_stage_metrics.as_mut() {
+                    bundle_stage_metrics.report();
+                    reported_slot = bundle_stage_metrics.reported_slot();
+                }
+                self.bundle_stage_metrics = new_bundle_stage_metrics;
+                reported_slot
+            }
+        }
+    }
+
+    pub(crate) fn increment_sanitize_transaction_result(
+        &mut self,
+        result: &Result<SanitizedBundle, DeserializedBundleError>,
+    ) {
+        if let Some(bundle_stage_metrics) = self.bundle_stage_metrics.as_mut() {
+            match result {
+                Ok(_) => {
+                    saturating_add_assign!(bundle_stage_metrics.sanitize_transaction_ok, 1);
+                }
+                Err(e) => match e {
+                    DeserializedBundleError::VoteOnlyMode => {
+                        saturating_add_assign!(
+                            bundle_stage_metrics.sanitize_transaction_vote_only_mode,
+                            1
+                        );
+                    }
+                    DeserializedBundleError::BlacklistedAccount => {
+                        saturating_add_assign!(
+                            bundle_stage_metrics.sanitize_transaction_blacklisted_account,
+                            1
+                        );
+                    }
+                    DeserializedBundleError::FailedToSerializeTransaction => {
+                        saturating_add_assign!(
+                            bundle_stage_metrics.sanitize_transaction_failed_to_serialize,
+                            1
+                        );
+                    }
+                    DeserializedBundleError::DuplicateTransaction => {
+                        saturating_add_assign!(
+                            bundle_stage_metrics.sanitize_transaction_duplicate_transaction,
+                            1
+                        );
+                    }
+                    DeserializedBundleError::FailedCheckTransactions => {
+                        saturating_add_assign!(
+                            bundle_stage_metrics.sanitize_transaction_failed_check,
+                            1
+                        );
+                    }
+                    DeserializedBundleError::FailedToSerializePacket => {
+                        saturating_add_assign!(
+                            bundle_stage_metrics.sanitize_transaction_failed_to_serialize,
+                            1
+                        );
+                    }
+                    DeserializedBundleError::EmptyBatch => {
+                        saturating_add_assign!(
+                            bundle_stage_metrics.sanitize_transaction_failed_empty_batch,
+                            1
+                        );
+                    }
+                    DeserializedBundleError::TooManyPackets => {
+                        saturating_add_assign!(
+                            bundle_stage_metrics.sanitize_transaction_failed_too_many_packets,
+                            1
+                        );
+                    }
+                    DeserializedBundleError::MarkedDiscard => {
+                        saturating_add_assign!(
+                            bundle_stage_metrics.sanitize_transaction_failed_marked_discard,
+                            1
+                        );
+                    }
+                    DeserializedBundleError::SignatureVerificationFailure => {
+                        saturating_add_assign!(
+                            bundle_stage_metrics.sanitize_transaction_failed_sig_verify_failed,
+                            1
+                        );
+                    }
+                },
+            }
+        }
+    }
+
+    pub fn increment_bundle_execution_result(&mut self, result: &Result<(), BundleExecutionError>) {
+        if let Some(bundle_stage_metrics) = &mut self.bundle_stage_metrics {
+            match result {
+                Ok(_) => {
+                    saturating_add_assign!(bundle_stage_metrics.execution_results_ok, 1);
+                }
+                Err(BundleExecutionError::PohRecordError(_))
+                | Err(BundleExecutionError::BankProcessingTimeLimitReached) => {
+                    saturating_add_assign!(
+                        bundle_stage_metrics.execution_results_poh_max_height,
+                        1
+                    );
+                }
+                Err(BundleExecutionError::TransactionFailure(
+                    LoadAndExecuteBundleError::ProcessingTimeExceeded(_),
+                )) => {
+                    saturating_add_assign!(bundle_stage_metrics.num_execution_timeouts, 1);
+                }
+                Err(BundleExecutionError::TransactionFailure(
+                    LoadAndExecuteBundleError::TransactionError { .. },
+                )) => {
+                    saturating_add_assign!(
+                        bundle_stage_metrics.execution_results_transaction_failures,
+                        1
+                    );
+                }
+                Err(BundleExecutionError::TransactionFailure(
+                    LoadAndExecuteBundleError::LockError { .. },
+                ))
+                | Err(BundleExecutionError::LockError) => {
+                    saturating_add_assign!(bundle_stage_metrics.num_lock_errors, 1);
+                }
+                Err(BundleExecutionError::ExceedsCostModel) => {
+                    saturating_add_assign!(
+                        bundle_stage_metrics.execution_results_exceeds_cost_model,
+                        1
+                    );
+                }
+                Err(BundleExecutionError::TipError(_)) => {
+                    saturating_add_assign!(bundle_stage_metrics.execution_results_tip_errors, 1);
+                }
+                Err(BundleExecutionError::TransactionFailure(
+                    LoadAndExecuteBundleError::InvalidPreOrPostAccounts,
+                )) => {
+                    saturating_add_assign!(bundle_stage_metrics.bad_argument, 1);
+                }
+            }
+        }
+    }
+
+    pub(crate) fn increment_sanitize_bundle_elapsed_us(&mut self, count: u64) {
+        if let Some(bundle_stage_metrics) = &mut self.bundle_stage_metrics {
+            saturating_add_assign!(bundle_stage_metrics.sanitize_bundle_elapsed_us, count);
+        }
+    }
+
+    pub(crate) fn increment_locked_bundle_elapsed_us(&mut self, count: u64) {
+        if let Some(bundle_stage_metrics) = &mut self.bundle_stage_metrics {
+            saturating_add_assign!(bundle_stage_metrics.locked_bundle_elapsed_us, count);
+        }
+    }
+
+    pub(crate) fn increment_num_init_tip_account_errors(&mut self, count: u64) {
+        if let Some(bundle_stage_metrics) = &mut self.bundle_stage_metrics {
+            saturating_add_assign!(bundle_stage_metrics.num_init_tip_account_errors, count);
+        }
+    }
+
+    pub(crate) fn increment_num_init_tip_account_ok(&mut self, count: u64) {
+        if let Some(bundle_stage_metrics) = &mut self.bundle_stage_metrics {
+            saturating_add_assign!(bundle_stage_metrics.num_init_tip_account_ok, count);
+        }
+    }
+
+    pub(crate) fn increment_num_change_tip_receiver_errors(&mut self, count: u64) {
+        if let Some(bundle_stage_metrics) = &mut self.bundle_stage_metrics {
+            saturating_add_assign!(bundle_stage_metrics.num_change_tip_receiver_errors, count);
+        }
+    }
+
+    pub(crate) fn increment_num_change_tip_receiver_ok(&mut self, count: u64) {
+        if let Some(bundle_stage_metrics) = &mut self.bundle_stage_metrics {
+            saturating_add_assign!(bundle_stage_metrics.num_change_tip_receiver_ok, count);
+        }
+    }
+
+    pub(crate) fn increment_change_tip_receiver_elapsed_us(&mut self, count: u64) {
+        if let Some(bundle_stage_metrics) = &mut self.bundle_stage_metrics {
+            saturating_add_assign!(bundle_stage_metrics.change_tip_receiver_elapsed_us, count);
+        }
+    }
+
+    pub(crate) fn increment_num_execution_retries(&mut self, count: u64) {
+        if let Some(bundle_stage_metrics) = &mut self.bundle_stage_metrics {
+            saturating_add_assign!(bundle_stage_metrics.num_execution_retries, count);
+        }
+    }
+
+    pub(crate) fn increment_execute_locked_bundles_elapsed_us(&mut self, count: u64) {
+        if let Some(bundle_stage_metrics) = &mut self.bundle_stage_metrics {
+            saturating_add_assign!(
+                bundle_stage_metrics.execute_locked_bundles_elapsed_us,
+                count
+            );
+        }
+    }
+}
+
+#[derive(Default)]
+pub struct BundleStageStats {
+    id: u32,
+    slot: u64,
+    is_reported: bool,
+
+    sanitize_transaction_ok: u64,
+    sanitize_transaction_vote_only_mode: u64,
+    sanitize_transaction_blacklisted_account: u64,
+    sanitize_transaction_failed_to_serialize: u64,
+    sanitize_transaction_duplicate_transaction: u64,
+    sanitize_transaction_failed_check: u64,
+    sanitize_bundle_elapsed_us: u64,
+    sanitize_transaction_failed_empty_batch: u64,
+    sanitize_transaction_failed_too_many_packets: u64,
+    sanitize_transaction_failed_marked_discard: u64,
+    sanitize_transaction_failed_sig_verify_failed: u64,
+
+    locked_bundle_elapsed_us: u64,
+
+    num_lock_errors: u64,
+
+    num_init_tip_account_errors: u64,
+    num_init_tip_account_ok: u64,
+
+    num_change_tip_receiver_errors: u64,
+    num_change_tip_receiver_ok: u64,
+    change_tip_receiver_elapsed_us: u64,
+
+    num_execution_timeouts: u64,
+    num_execution_retries: u64,
+
+    execute_locked_bundles_elapsed_us: u64,
+
+    execution_results_ok: u64,
+    execution_results_poh_max_height: u64,
+    execution_results_transaction_failures: u64,
+    execution_results_exceeds_cost_model: u64,
+    execution_results_tip_errors: u64,
+    execution_results_max_retries: u64,
+
+    bad_argument: u64,
+}
+
+impl BundleStageStats {
+    pub fn new(id: u32, slot: Slot) -> BundleStageStats {
+        BundleStageStats {
+            id,
+            slot,
+            is_reported: false,
+            ..BundleStageStats::default()
+        }
+    }
+
+    /// Returns `Some(self.slot)` if the metrics have been reported, otherwise returns None
+    fn reported_slot(&self) -> Option<Slot> {
+        if self.is_reported {
+            Some(self.slot)
+        } else {
+            None
+        }
+    }
+
+    pub fn report(&mut self) {
+        self.is_reported = true;
+
+        datapoint_info!(
+            "bundle_stage-stats",
+            ("id", self.id, i64),
+            ("slot", self.slot, i64),
+            ("num_sanitized_ok", self.sanitize_transaction_ok, i64),
+            (
+                "sanitize_transaction_vote_only_mode",
+                self.sanitize_transaction_vote_only_mode,
+                i64
+            ),
+            (
+                "sanitize_transaction_blacklisted_account",
+                self.sanitize_transaction_blacklisted_account,
+                i64
+            ),
+            (
+                "sanitize_transaction_failed_to_serialize",
+                self.sanitize_transaction_failed_to_serialize,
+                i64
+            ),
+            (
+                "sanitize_transaction_duplicate_transaction",
+                self.sanitize_transaction_duplicate_transaction,
+                i64
+            ),
+            (
+                "sanitize_transaction_failed_check",
+                self.sanitize_transaction_failed_check,
+                i64
+            ),
+            (
+                "sanitize_bundle_elapsed_us",
+                self.sanitize_bundle_elapsed_us,
+                i64
+            ),
+            (
+                "sanitize_transaction_failed_empty_batch",
+                self.sanitize_transaction_failed_empty_batch,
+                i64
+            ),
+            (
+                "sanitize_transaction_failed_too_many_packets",
+                self.sanitize_transaction_failed_too_many_packets,
+                i64
+            ),
+            (
+                "sanitize_transaction_failed_marked_discard",
+                self.sanitize_transaction_failed_marked_discard,
+                i64
+            ),
+            (
+                "sanitize_transaction_failed_sig_verify_failed",
+                self.sanitize_transaction_failed_sig_verify_failed,
+                i64
+            ),
+            (
+                "locked_bundle_elapsed_us",
+                self.locked_bundle_elapsed_us,
+                i64
+            ),
+            ("num_lock_errors", self.num_lock_errors, i64),
+            (
+                "num_init_tip_account_errors",
+                self.num_init_tip_account_errors,
+                i64
+            ),
+            ("num_init_tip_account_ok", self.num_init_tip_account_ok, i64),
+            (
+                "num_change_tip_receiver_errors",
+                self.num_change_tip_receiver_errors,
+                i64
+            ),
+            (
+                "num_change_tip_receiver_ok",
+                self.num_change_tip_receiver_ok,
+                i64
+            ),
+            (
+                "change_tip_receiver_elapsed_us",
+                self.change_tip_receiver_elapsed_us,
+                i64
+            ),
+            ("num_execution_timeouts", self.num_execution_timeouts, i64),
+            ("num_execution_retries", self.num_execution_retries, i64),
+            (
+                "execute_locked_bundles_elapsed_us",
+                self.execute_locked_bundles_elapsed_us,
+                i64
+            ),
+            ("execution_results_ok", self.execution_results_ok, i64),
+            (
+                "execution_results_poh_max_height",
+                self.execution_results_poh_max_height,
+                i64
+            ),
+            (
+                "execution_results_transaction_failures",
+                self.execution_results_transaction_failures,
+                i64
+            ),
+            (
+                "execution_results_exceeds_cost_model",
+                self.execution_results_exceeds_cost_model,
+                i64
+            ),
+            (
+                "execution_results_tip_errors",
+                self.execution_results_tip_errors,
+                i64
+            ),
+            (
+                "execution_results_max_retries",
+                self.execution_results_max_retries,
+                i64
+            ),
+            ("bad_argument", self.bad_argument, i64)
+        );
+    }
+}
diff --git a/core/src/bundle_stage/committer.rs b/core/src/bundle_stage/committer.rs
new file mode 100644
index 0000000000..ae87c25aaf
--- /dev/null
+++ b/core/src/bundle_stage/committer.rs
@@ -0,0 +1,221 @@
+use {
+    crate::{
+        banking_stage::committer::CommitTransactionDetails,
+        leader_slot_banking_stage_timing_metrics::LeaderExecuteAndCommitTimings,
+    },
+    solana_bundle::bundle_execution::LoadAndExecuteBundleOutput,
+    solana_ledger::blockstore_processor::TransactionStatusSender,
+    solana_measure::measure_us,
+    solana_runtime::{
+        bank::{
+            Bank, CommitTransactionCounts, TransactionBalances, TransactionBalancesSet,
+            TransactionResults,
+        },
+        bank_utils,
+        prioritization_fee_cache::PrioritizationFeeCache,
+        vote_sender_types::ReplayVoteSender,
+    },
+    solana_sdk::{saturating_add_assign, transaction::SanitizedTransaction},
+    solana_transaction_status::{
+        token_balances::{TransactionTokenBalances, TransactionTokenBalancesSet},
+        PreBalanceInfo,
+    },
+    std::sync::Arc,
+};
+
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct CommitBundleDetails {
+    pub commit_transaction_details: Vec<Vec<CommitTransactionDetails>>,
+}
+
+pub struct Committer {
+    transaction_status_sender: Option<TransactionStatusSender>,
+    replay_vote_sender: ReplayVoteSender,
+    prioritization_fee_cache: Arc<PrioritizationFeeCache>,
+}
+
+impl Committer {
+    pub fn new(
+        transaction_status_sender: Option<TransactionStatusSender>,
+        replay_vote_sender: ReplayVoteSender,
+        prioritization_fee_cache: Arc<PrioritizationFeeCache>,
+    ) -> Self {
+        Self {
+            transaction_status_sender,
+            replay_vote_sender,
+            prioritization_fee_cache,
+        }
+    }
+
+    pub(crate) fn transaction_status_sender_enabled(&self) -> bool {
+        self.transaction_status_sender.is_some()
+    }
+
+    /// Very similar to Committer::commit_transactions, but works with bundles.
+    /// The main difference is there's multiple non-parallelizable transaction vectors to commit
+    /// and post-balances are collected after execution instead of from the bank in Self::collect_balances_and_send_status_batch.
+    #[allow(clippy::too_many_arguments)]
+    pub(crate) fn commit_bundle<'a>(
+        &self,
+        bundle_execution_output: &'a mut LoadAndExecuteBundleOutput<'a>,
+        mut starting_transaction_index: Option<usize>,
+        bank: &Arc<Bank>,
+        execute_and_commit_timings: &mut LeaderExecuteAndCommitTimings,
+    ) -> (u64, CommitBundleDetails) {
+        let (last_blockhash, lamports_per_signature) =
+            bank.last_blockhash_and_lamports_per_signature();
+
+        let transaction_output = bundle_execution_output.bundle_transaction_results_mut();
+
+        let (commit_transaction_details, commit_times): (Vec<_>, Vec<_>) = transaction_output
+            .iter_mut()
+            .map(|bundle_results| {
+                let committed_transactions_count = bundle_results
+                    .load_and_execute_transactions_output()
+                    .executed_transactions_count
+                    as u64;
+
+                let committed_non_vote_transactions_count = bundle_results
+                    .load_and_execute_transactions_output()
+                    .executed_non_vote_transactions_count
+                    as u64;
+
+                let committed_with_failure_result_count = bundle_results
+                    .load_and_execute_transactions_output()
+                    .executed_transactions_count
+                    .saturating_sub(
+                        bundle_results
+                            .load_and_execute_transactions_output()
+                            .executed_with_successful_result_count,
+                    ) as u64;
+
+                let signature_count = bundle_results
+                    .load_and_execute_transactions_output()
+                    .signature_count;
+
+                let sanitized_transactions = bundle_results.transactions().to_vec();
+                let execution_results = bundle_results.execution_results().to_vec();
+
+                let loaded_transactions = bundle_results.loaded_transactions_mut();
+                debug!("loaded_transactions: {:?}", loaded_transactions);
+
+                let (tx_results, commit_time_us) = measure_us!(bank.commit_transactions(
+                    &sanitized_transactions,
+                    loaded_transactions,
+                    execution_results,
+                    last_blockhash,
+                    lamports_per_signature,
+                    CommitTransactionCounts {
+                        committed_transactions_count,
+                        committed_non_vote_transactions_count,
+                        committed_with_failure_result_count,
+                        signature_count,
+                    },
+                    &mut execute_and_commit_timings.execute_timings,
+                ));
+
+                let commit_transaction_statuses: Vec<_> = tx_results
+                    .execution_results
+                    .iter()
+                    .map(|execution_result| match execution_result.details() {
+                        Some(details) => CommitTransactionDetails::Committed {
+                            compute_units: details.executed_units,
+                        },
+                        None => CommitTransactionDetails::NotCommitted,
+                    })
+                    .collect();
+
+                let ((), find_and_send_votes_us) = measure_us!({
+                    bank_utils::find_and_send_votes(
+                        &sanitized_transactions,
+                        &tx_results,
+                        Some(&self.replay_vote_sender),
+                    );
+
+                    let post_balance_info = bundle_results.post_balance_info().clone();
+                    let pre_balance_info = bundle_results.pre_balance_info();
+
+                    let num_committed = tx_results
+                        .execution_results
+                        .iter()
+                        .filter(|r| r.was_executed())
+                        .count();
+
+                    self.collect_balances_and_send_status_batch(
+                        tx_results,
+                        bank,
+                        sanitized_transactions,
+                        pre_balance_info,
+                        post_balance_info,
+                        starting_transaction_index,
+                    );
+
+                    // NOTE: we're doing batched records, so we need to increment the poh starting_transaction_index
+                    // by number committed so the next batch will have the correct starting_transaction_index
+                    starting_transaction_index =
+                        starting_transaction_index.map(|starting_transaction_index| {
+                            starting_transaction_index.saturating_add(num_committed)
+                        });
+
+                    self.prioritization_fee_cache
+                        .update(bank, bundle_results.executed_transactions().into_iter());
+                });
+                saturating_add_assign!(
+                    execute_and_commit_timings.find_and_send_votes_us,
+                    find_and_send_votes_us
+                );
+
+                (commit_transaction_statuses, commit_time_us)
+            })
+            .unzip();
+
+        (
+            commit_times.iter().sum(),
+            CommitBundleDetails {
+                commit_transaction_details,
+            },
+        )
+    }
+
+    fn collect_balances_and_send_status_batch(
+        &self,
+        tx_results: TransactionResults,
+        bank: &Arc<Bank>,
+        sanitized_transactions: Vec<SanitizedTransaction>,
+        pre_balance_info: &mut PreBalanceInfo,
+        (post_balances, post_token_balances): (TransactionBalances, TransactionTokenBalances),
+        starting_transaction_index: Option<usize>,
+    ) {
+        if let Some(transaction_status_sender) = &self.transaction_status_sender {
+            let mut transaction_index = starting_transaction_index.unwrap_or_default();
+            let batch_transaction_indexes: Vec<_> = tx_results
+                .execution_results
+                .iter()
+                .map(|result| {
+                    if result.was_executed() {
+                        let this_transaction_index = transaction_index;
+                        saturating_add_assign!(transaction_index, 1);
+                        this_transaction_index
+                    } else {
+                        0
+                    }
+                })
+                .collect();
+            transaction_status_sender.send_transaction_status_batch(
+                bank.clone(),
+                sanitized_transactions,
+                tx_results.execution_results,
+                TransactionBalancesSet::new(
+                    std::mem::take(&mut pre_balance_info.native),
+                    post_balances,
+                ),
+                TransactionTokenBalancesSet::new(
+                    std::mem::take(&mut pre_balance_info.token),
+                    post_token_balances,
+                ),
+                tx_results.rent_debits,
+                batch_transaction_indexes,
+            );
+        }
+    }
+}
diff --git a/core/src/bundle_stage/result.rs b/core/src/bundle_stage/result.rs
new file mode 100644
index 0000000000..3370251791
--- /dev/null
+++ b/core/src/bundle_stage/result.rs
@@ -0,0 +1,41 @@
+use {
+    crate::{
+        bundle_stage::bundle_account_locker::BundleAccountLockerError, tip_manager::TipPaymentError,
+    },
+    anchor_lang::error::Error,
+    solana_bundle::bundle_execution::LoadAndExecuteBundleError,
+    solana_poh::poh_recorder::PohRecorderError,
+    thiserror::Error,
+};
+
+pub type BundleExecutionResult<T> = Result<T, BundleExecutionError>;
+
+#[derive(Error, Debug, Clone)]
+pub enum BundleExecutionError {
+    #[error("PoH record error: {0}")]
+    PohRecordError(#[from] PohRecorderError),
+
+    #[error("Bank is done processing")]
+    BankProcessingDone,
+
+    #[error("Execution error: {0}")]
+    ExecutionError(#[from] LoadAndExecuteBundleError),
+
+    #[error("The bundle exceeds the cost model")]
+    ExceedsCostModel,
+
+    #[error("Tip error {0}")]
+    TipError(#[from] TipPaymentError),
+
+    #[error("Error locking bundle")]
+    LockError(#[from] BundleAccountLockerError),
+}
+
+impl From<anchor_lang::error::Error> for TipPaymentError {
+    fn from(anchor_err: Error) -> Self {
+        match anchor_err {
+            Error::AnchorError(e) => Self::AnchorError(e.error_msg),
+            Error::ProgramError(e) => Self::AnchorError(e.to_string()),
+        }
+    }
+}
diff --git a/core/src/consensus_cache_updater.rs b/core/src/consensus_cache_updater.rs
new file mode 100644
index 0000000000..1bd75554c0
--- /dev/null
+++ b/core/src/consensus_cache_updater.rs
@@ -0,0 +1,52 @@
+use {
+    solana_runtime::bank::Bank,
+    solana_sdk::{clock::Epoch, pubkey::Pubkey},
+    std::collections::HashSet,
+};
+
+#[derive(Default)]
+pub(crate) struct ConsensusCacheUpdater {
+    last_epoch_updated: Epoch,
+    consensus_accounts_cache: HashSet<Pubkey>,
+}
+
+impl ConsensusCacheUpdater {
+    pub(crate) fn consensus_accounts_cache(&self) -> &HashSet<Pubkey> {
+        &self.consensus_accounts_cache
+    }
+
+    /// Builds a HashSet of all consensus related accounts for the Bank's epoch
+    fn get_consensus_accounts(bank: &Bank) -> HashSet<Pubkey> {
+        let mut consensus_accounts: HashSet<Pubkey> = HashSet::new();
+        if let Some(epoch_stakes) = bank.epoch_stakes(bank.epoch()) {
+            // votes use the following accounts:
+            // - vote_account pubkey: writeable
+            // - authorized_voter_pubkey: read-only
+            // - node_keypair pubkey: payer (writeable)
+            let node_id_vote_accounts = epoch_stakes.node_id_to_vote_accounts();
+
+            let vote_accounts = node_id_vote_accounts
+                .values()
+                .flat_map(|v| v.vote_accounts.clone());
+
+            // vote_account
+            consensus_accounts.extend(vote_accounts.into_iter());
+            // authorized_voter_pubkey
+            consensus_accounts.extend(epoch_stakes.epoch_authorized_voters().keys());
+            // node_keypair
+            consensus_accounts.extend(epoch_stakes.node_id_to_vote_accounts().keys());
+        }
+        consensus_accounts
+    }
+
+    /// Updates consensus-related accounts on epoch boundaries
+    pub(crate) fn maybe_update(&mut self, bank: &Bank) -> bool {
+        if bank.epoch() > self.last_epoch_updated {
+            self.consensus_accounts_cache = Self::get_consensus_accounts(bank);
+            self.last_epoch_updated = bank.epoch();
+            true
+        } else {
+            false
+        }
+    }
+}
diff --git a/core/src/immutable_deserialized_bundle.rs b/core/src/immutable_deserialized_bundle.rs
new file mode 100644
index 0000000000..b3d82741fd
--- /dev/null
+++ b/core/src/immutable_deserialized_bundle.rs
@@ -0,0 +1,483 @@
+use {
+    crate::{
+        immutable_deserialized_packet::ImmutableDeserializedPacket, packet_bundle::PacketBundle,
+    },
+    solana_perf::sigverify::verify_packet,
+    solana_runtime::{bank::Bank, transaction_error_metrics::TransactionErrorMetrics},
+    solana_sdk::{
+        bundle::SanitizedBundle, clock::MAX_PROCESSING_AGE, pubkey::Pubkey, signature::Signature,
+        transaction::SanitizedTransaction,
+    },
+    std::{
+        collections::{hash_map::RandomState, HashSet},
+        iter::repeat,
+    },
+    thiserror::Error,
+};
+
+#[derive(Debug, Error, Eq, PartialEq)]
+pub enum DeserializedBundleError {
+    #[error("FailedToSerializePacket")]
+    FailedToSerializePacket,
+
+    #[error("EmptyBatch")]
+    EmptyBatch,
+
+    #[error("TooManyPackets")]
+    TooManyPackets,
+
+    #[error("MarkedDiscard")]
+    MarkedDiscard,
+
+    #[error("SignatureVerificationFailure")]
+    SignatureVerificationFailure,
+
+    #[error("Bank is in vote-only mode")]
+    VoteOnlyMode,
+
+    #[error("Bundle mentions blacklisted account")]
+    BlacklistedAccount,
+
+    #[error("Bundle contains a transaction that failed to serialize")]
+    FailedToSerializeTransaction,
+
+    #[error("Bundle contains a duplicate transaction")]
+    DuplicateTransaction,
+
+    #[error("Bundle failed check_transactions")]
+    FailedCheckTransactions,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct ImmutableDeserializedBundle {
+    bundle_id: String,
+    packets: Vec<ImmutableDeserializedPacket>,
+}
+
+impl ImmutableDeserializedBundle {
+    pub fn new(
+        bundle: &mut PacketBundle,
+        max_len: Option<usize>,
+    ) -> Result<Self, DeserializedBundleError> {
+        // Checks: non-zero, less than some length, marked for discard, signature verification failed, failed to sanitize to
+        // ImmutableDeserializedPacket
+        if bundle.batch.is_empty() {
+            return Err(DeserializedBundleError::EmptyBatch);
+        }
+        if max_len
+            .map(|max_len| bundle.batch.len() > max_len)
+            .unwrap_or(false)
+        {
+            return Err(DeserializedBundleError::TooManyPackets);
+        }
+        if bundle.batch.iter().any(|p| p.meta().discard()) {
+            return Err(DeserializedBundleError::MarkedDiscard);
+        }
+        if bundle.batch.iter_mut().any(|p| !verify_packet(p, false)) {
+            return Err(DeserializedBundleError::SignatureVerificationFailure);
+        }
+
+        let immutable_packets: Vec<_> = bundle
+            .batch
+            .iter()
+            .filter_map(|p| ImmutableDeserializedPacket::new(p.clone()).ok())
+            .collect();
+
+        if bundle.batch.len() != immutable_packets.len() {
+            return Err(DeserializedBundleError::FailedToSerializePacket);
+        }
+
+        Ok(Self {
+            bundle_id: bundle.bundle_id.clone(),
+            packets: immutable_packets,
+        })
+    }
+
+    #[allow(clippy::len_without_is_empty)]
+    pub fn len(&self) -> usize {
+        self.packets.len()
+    }
+
+    pub fn bundle_id(&self) -> &str {
+        &self.bundle_id
+    }
+
+    /// A bundle has the following requirements:
+    /// - all transactions must be sanitiz-able
+    /// - no duplicate signatures
+    /// - must not contain a blacklisted account
+    /// - can't already be processed or contain a bad blockhash
+    pub fn build_sanitized_bundle(
+        &self,
+        bank: &Bank,
+        blacklisted_accounts: &HashSet<Pubkey>,
+        transaction_error_metrics: &mut TransactionErrorMetrics,
+    ) -> Result<SanitizedBundle, DeserializedBundleError> {
+        if bank.vote_only_bank() {
+            return Err(DeserializedBundleError::VoteOnlyMode);
+        }
+
+        let transactions: Vec<SanitizedTransaction> = self
+            .packets
+            .iter()
+            .filter_map(|p| {
+                p.build_sanitized_transaction(&bank.feature_set, bank.vote_only_bank(), bank)
+            })
+            .collect();
+
+        if self.packets.len() != transactions.len() {
+            return Err(DeserializedBundleError::FailedToSerializeTransaction);
+        }
+
+        let unique_signatures: HashSet<&Signature, RandomState> =
+            HashSet::from_iter(transactions.iter().map(|tx| tx.signature()));
+        if unique_signatures.len() != transactions.len() {
+            return Err(DeserializedBundleError::DuplicateTransaction);
+        }
+
+        let contains_blacklisted_account = transactions.iter().any(|tx| {
+            tx.message()
+                .account_keys()
+                .iter()
+                .any(|acc| blacklisted_accounts.contains(acc))
+        });
+
+        if contains_blacklisted_account {
+            return Err(DeserializedBundleError::BlacklistedAccount);
+        }
+
+        // assume everything locks okay to check for already-processed transaction or expired/invalid blockhash
+        let lock_results: Vec<_> = repeat(Ok(())).take(transactions.len()).collect();
+        let check_results = bank.check_transactions(
+            &transactions,
+            &lock_results,
+            MAX_PROCESSING_AGE,
+            transaction_error_metrics,
+        );
+
+        if check_results.iter().any(|r| r.0.is_err()) {
+            return Err(DeserializedBundleError::FailedCheckTransactions);
+        }
+
+        Ok(SanitizedBundle {
+            transactions,
+            bundle_id: self.bundle_id.clone(),
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {
+        crate::{
+            immutable_deserialized_bundle::{DeserializedBundleError, ImmutableDeserializedBundle},
+            packet_bundle::PacketBundle,
+        },
+        solana_client::rpc_client::SerializableTransaction,
+        solana_ledger::genesis_utils::create_genesis_config,
+        solana_perf::packet::PacketBatch,
+        solana_runtime::{
+            bank::{Bank, NewBankOptions},
+            genesis_utils::GenesisConfigInfo,
+            transaction_error_metrics::TransactionErrorMetrics,
+        },
+        solana_sdk::{
+            hash::Hash,
+            packet::Packet,
+            pubkey::Pubkey,
+            signature::{Keypair, Signer},
+            system_transaction::transfer,
+        },
+        std::{collections::HashSet, sync::Arc},
+    };
+
+    /// Happy case
+    #[test]
+    fn test_simple_get_sanitized_bundle() {
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank = Arc::new(Bank::new_no_wallclock_throttle_for_tests(&genesis_config));
+
+        let kp = Keypair::new();
+
+        let tx0 = transfer(&mint_keypair, &kp.pubkey(), 500, genesis_config.hash());
+
+        let tx1 = transfer(&mint_keypair, &kp.pubkey(), 501, genesis_config.hash());
+
+        let bundle = ImmutableDeserializedBundle::new(
+            &mut PacketBundle {
+                batch: PacketBatch::new(vec![
+                    Packet::from_data(None, &tx0).unwrap(),
+                    Packet::from_data(None, &tx1).unwrap(),
+                ]),
+                bundle_id: String::default(),
+            },
+            None,
+        )
+        .unwrap();
+
+        let mut transaction_errors = TransactionErrorMetrics::default();
+        let sanitized_bundle = bundle
+            .build_sanitized_bundle(&bank, &HashSet::default(), &mut transaction_errors)
+            .unwrap();
+        assert_eq!(sanitized_bundle.transactions.len(), 2);
+        assert_eq!(
+            sanitized_bundle.transactions[0].signature(),
+            tx0.get_signature()
+        );
+        assert_eq!(
+            sanitized_bundle.transactions[1].signature(),
+            tx1.get_signature()
+        );
+    }
+
+    #[test]
+    fn test_empty_batch_fails_to_init() {
+        assert_eq!(
+            ImmutableDeserializedBundle::new(
+                &mut PacketBundle {
+                    batch: PacketBatch::new(vec![]),
+                    bundle_id: String::default(),
+                },
+                None,
+            ),
+            Err(DeserializedBundleError::EmptyBatch)
+        );
+    }
+
+    #[test]
+    fn test_too_many_packets_fails_to_init() {
+        let kp = Keypair::new();
+
+        assert_eq!(
+            ImmutableDeserializedBundle::new(
+                &mut PacketBundle {
+                    batch: PacketBatch::new(
+                        (0..10)
+                            .map(|i| {
+                                Packet::from_data(
+                                    None,
+                                    transfer(&kp, &kp.pubkey(), i, Hash::default()),
+                                )
+                                .unwrap()
+                            })
+                            .collect()
+                    ),
+                    bundle_id: String::default(),
+                },
+                Some(5),
+            ),
+            Err(DeserializedBundleError::TooManyPackets)
+        );
+    }
+
+    #[test]
+    fn test_packets_marked_discard_fails_to_init() {
+        let kp = Keypair::new();
+
+        let mut packet =
+            Packet::from_data(None, transfer(&kp, &kp.pubkey(), 100, Hash::default())).unwrap();
+        packet.meta_mut().set_discard(true);
+
+        assert_eq!(
+            ImmutableDeserializedBundle::new(
+                &mut PacketBundle {
+                    batch: PacketBatch::new(vec![packet]),
+                    bundle_id: String::default(),
+                },
+                Some(5),
+            ),
+            Err(DeserializedBundleError::MarkedDiscard)
+        );
+    }
+
+    #[test]
+    fn test_bad_signature_fails_to_init() {
+        let kp0 = Keypair::new();
+        let kp1 = Keypair::new();
+
+        let mut tx0 = transfer(&kp0, &kp0.pubkey(), 100, Hash::default());
+        let tx1 = transfer(&kp1, &kp0.pubkey(), 100, Hash::default());
+        tx0.signatures = tx1.signatures;
+
+        assert_eq!(
+            ImmutableDeserializedBundle::new(
+                &mut PacketBundle {
+                    batch: PacketBatch::new(vec![Packet::from_data(None, tx0).unwrap()]),
+                    bundle_id: String::default(),
+                },
+                None
+            ),
+            Err(DeserializedBundleError::SignatureVerificationFailure)
+        );
+    }
+
+    #[test]
+    fn test_vote_only_bank_fails_to_build() {
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let parent = Arc::new(Bank::new_no_wallclock_throttle_for_tests(&genesis_config));
+        let vote_only_bank = Arc::new(Bank::new_from_parent_with_options(
+            &parent,
+            &Pubkey::new_unique(),
+            1,
+            NewBankOptions {
+                vote_only_bank: true,
+            },
+        ));
+
+        let kp = Keypair::new();
+
+        let tx0 = transfer(&mint_keypair, &kp.pubkey(), 500, genesis_config.hash());
+
+        let bundle = ImmutableDeserializedBundle::new(
+            &mut PacketBundle {
+                batch: PacketBatch::new(vec![Packet::from_data(None, tx0).unwrap()]),
+                bundle_id: String::default(),
+            },
+            None,
+        )
+        .unwrap();
+
+        let mut transaction_errors = TransactionErrorMetrics::default();
+        assert_matches!(
+            bundle.build_sanitized_bundle(
+                &vote_only_bank,
+                &HashSet::default(),
+                &mut transaction_errors
+            ),
+            Err(DeserializedBundleError::VoteOnlyMode)
+        );
+    }
+
+    #[test]
+    fn test_duplicate_signature_fails_to_build() {
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank = Arc::new(Bank::new_no_wallclock_throttle_for_tests(&genesis_config));
+
+        let kp = Keypair::new();
+
+        let tx0 = transfer(&mint_keypair, &kp.pubkey(), 500, genesis_config.hash());
+
+        let bundle = ImmutableDeserializedBundle::new(
+            &mut PacketBundle {
+                batch: PacketBatch::new(vec![
+                    Packet::from_data(None, &tx0).unwrap(),
+                    Packet::from_data(None, &tx0).unwrap(),
+                ]),
+                bundle_id: String::default(),
+            },
+            None,
+        )
+        .unwrap();
+
+        let mut transaction_errors = TransactionErrorMetrics::default();
+        assert_matches!(
+            bundle.build_sanitized_bundle(&bank, &HashSet::default(), &mut transaction_errors),
+            Err(DeserializedBundleError::DuplicateTransaction)
+        );
+    }
+
+    #[test]
+    fn test_blacklisted_account_fails_to_build() {
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank = Arc::new(Bank::new_no_wallclock_throttle_for_tests(&genesis_config));
+
+        let kp = Keypair::new();
+
+        let tx0 = transfer(&mint_keypair, &kp.pubkey(), 500, genesis_config.hash());
+
+        let bundle = ImmutableDeserializedBundle::new(
+            &mut PacketBundle {
+                batch: PacketBatch::new(vec![Packet::from_data(None, tx0).unwrap()]),
+                bundle_id: String::default(),
+            },
+            None,
+        )
+        .unwrap();
+
+        let mut transaction_errors = TransactionErrorMetrics::default();
+        assert_matches!(
+            bundle.build_sanitized_bundle(
+                &bank,
+                &HashSet::from([kp.pubkey()]),
+                &mut transaction_errors
+            ),
+            Err(DeserializedBundleError::BlacklistedAccount)
+        );
+    }
+
+    #[test]
+    fn test_already_processed_tx_fails_to_build() {
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank = Arc::new(Bank::new_no_wallclock_throttle_for_tests(&genesis_config));
+
+        let kp = Keypair::new();
+
+        let tx0 = transfer(&mint_keypair, &kp.pubkey(), 500, genesis_config.hash());
+
+        bank.process_transaction(&tx0).unwrap();
+
+        let bundle = ImmutableDeserializedBundle::new(
+            &mut PacketBundle {
+                batch: PacketBatch::new(vec![Packet::from_data(None, tx0).unwrap()]),
+                bundle_id: String::default(),
+            },
+            None,
+        )
+        .unwrap();
+
+        let mut transaction_errors = TransactionErrorMetrics::default();
+        assert_matches!(
+            bundle.build_sanitized_bundle(&bank, &HashSet::default(), &mut transaction_errors),
+            Err(DeserializedBundleError::FailedCheckTransactions)
+        );
+    }
+
+    #[test]
+    fn test_bad_blockhash_fails_to_build() {
+        let GenesisConfigInfo {
+            genesis_config,
+            mint_keypair,
+            ..
+        } = create_genesis_config(10_000);
+        let bank = Arc::new(Bank::new_no_wallclock_throttle_for_tests(&genesis_config));
+
+        let kp = Keypair::new();
+
+        let tx0 = transfer(&mint_keypair, &kp.pubkey(), 500, Hash::default());
+
+        let bundle = ImmutableDeserializedBundle::new(
+            &mut PacketBundle {
+                batch: PacketBatch::new(vec![Packet::from_data(None, tx0).unwrap()]),
+                bundle_id: String::default(),
+            },
+            None,
+        )
+        .unwrap();
+
+        let mut transaction_errors = TransactionErrorMetrics::default();
+        assert_matches!(
+            bundle.build_sanitized_bundle(&bank, &HashSet::default(), &mut transaction_errors),
+            Err(DeserializedBundleError::FailedCheckTransactions)
+        );
+    }
+}
diff --git a/core/src/latest_unprocessed_votes.rs b/core/src/latest_unprocessed_votes.rs
index db606a4a2b..8dde88be74 100644
--- a/core/src/latest_unprocessed_votes.rs
+++ b/core/src/latest_unprocessed_votes.rs
@@ -136,7 +136,7 @@ pub(crate) fn weighted_random_order_by_stake<'a>(
 }
 
 #[derive(Default, Debug)]
-pub(crate) struct VoteBatchInsertionMetrics {
+pub struct VoteBatchInsertionMetrics {
     pub(crate) num_dropped_gossip: usize,
     pub(crate) num_dropped_tpu: usize,
 }
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 6747732231..988142e06a 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -14,6 +14,7 @@ pub mod ancestor_hashes_service;
 pub mod banking_stage;
 pub mod banking_trace;
 pub mod broadcast_stage;
+pub mod bundle_stage;
 pub mod cache_block_meta_service;
 pub mod cluster_info_vote_listener;
 pub mod cluster_nodes;
@@ -23,6 +24,7 @@ pub mod cluster_slots_service;
 pub mod commitment_service;
 pub mod completed_data_sets_service;
 pub mod consensus;
+pub mod consensus_cache_updater;
 pub mod cost_update_service;
 pub mod drop_bank_service;
 pub mod duplicate_repair_status;
@@ -31,6 +33,7 @@ pub mod fork_choice;
 pub mod forward_packet_batches_by_accounts;
 pub mod gen_keys;
 pub mod heaviest_subtree_fork_choice;
+pub mod immutable_deserialized_bundle;
 pub mod immutable_deserialized_packet;
 mod latest_unprocessed_votes;
 pub mod latest_validator_votes_for_frozen_banks;
@@ -42,11 +45,13 @@ pub mod multi_iterator_scanner;
 pub mod next_leader;
 pub mod optimistic_confirmation_verifier;
 pub mod outstanding_requests;
+pub mod packet_bundle;
 pub mod packet_deserializer;
 pub mod packet_threshold;
 pub mod poh_timing_report_service;
 pub mod poh_timing_reporter;
 pub mod progress_map;
+pub mod proxy;
 pub mod qos_service;
 pub mod read_write_account_set;
 pub mod repair_generic_traversal;
@@ -70,6 +75,7 @@ pub mod snapshot_packager_service;
 pub mod staked_nodes_updater_service;
 pub mod stats_reporter_service;
 pub mod system_monitor_service;
+pub mod tip_manager;
 mod tower1_14_11;
 mod tower1_7_14;
 pub mod tower_storage;
@@ -107,3 +113,41 @@ extern crate solana_frozen_abi_macro;
 #[cfg(test)]
 #[macro_use]
 extern crate matches;
+
+use {
+    solana_sdk::packet::{Meta, Packet, PacketFlags, PACKET_DATA_SIZE},
+    std::{
+        cmp::min,
+        net::{IpAddr, Ipv4Addr},
+    },
+};
+
+const UNKNOWN_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
+
+// NOTE: last profiled at around 180ns
+pub fn proto_packet_to_packet(p: jito_protos::proto::packet::Packet) -> Packet {
+    let mut data = [0; PACKET_DATA_SIZE];
+    let copy_len = min(data.len(), p.data.len());
+    data[..copy_len].copy_from_slice(&p.data[..copy_len]);
+    let mut packet = Packet::new(data, Meta::default());
+    if let Some(meta) = p.meta {
+        packet.meta_mut().size = meta.size as usize;
+        packet.meta_mut().addr = meta.addr.parse().unwrap_or(UNKNOWN_IP);
+        packet.meta_mut().port = meta.port as u16;
+        if let Some(flags) = meta.flags {
+            if flags.simple_vote_tx {
+                packet.meta_mut().flags.insert(PacketFlags::SIMPLE_VOTE_TX);
+            }
+            if flags.forwarded {
+                packet.meta_mut().flags.insert(PacketFlags::FORWARDED);
+            }
+            if flags.tracer_packet {
+                packet.meta_mut().flags.insert(PacketFlags::TRACER_PACKET);
+            }
+            if flags.repair {
+                packet.meta_mut().flags.insert(PacketFlags::REPAIR);
+            }
+        }
+    }
+    packet
+}
diff --git a/core/src/packet_bundle.rs b/core/src/packet_bundle.rs
new file mode 100644
index 0000000000..2158f37414
--- /dev/null
+++ b/core/src/packet_bundle.rs
@@ -0,0 +1,7 @@
+use solana_perf::packet::PacketBatch;
+
+#[derive(Clone, Debug)]
+pub struct PacketBundle {
+    pub batch: PacketBatch,
+    pub bundle_id: String,
+}
diff --git a/core/src/proxy/auth.rs b/core/src/proxy/auth.rs
new file mode 100644
index 0000000000..39821e12ef
--- /dev/null
+++ b/core/src/proxy/auth.rs
@@ -0,0 +1,185 @@
+use {
+    crate::proxy::ProxyError,
+    chrono::Utc,
+    jito_protos::proto::auth::{
+        auth_service_client::AuthServiceClient, GenerateAuthChallengeRequest,
+        GenerateAuthTokensRequest, RefreshAccessTokenRequest, Role, Token,
+    },
+    solana_gossip::cluster_info::ClusterInfo,
+    solana_sdk::signature::{Keypair, Signer},
+    std::{
+        sync::{Arc, Mutex},
+        time::Duration,
+    },
+    tokio::time::timeout,
+    tonic::{service::Interceptor, transport::Channel, Code, Request, Status},
+};
+
+/// Interceptor responsible for adding the access token to request headers.
+pub(crate) struct AuthInterceptor {
+    /// The token added to each request header.
+    access_token: Arc<Mutex<Token>>,
+}
+
+impl AuthInterceptor {
+    pub(crate) fn new(access_token: Arc<Mutex<Token>>) -> Self {
+        Self { access_token }
+    }
+}
+
+impl Interceptor for AuthInterceptor {
+    fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
+        request.metadata_mut().insert(
+            "authorization",
+            format!("Bearer {}", self.access_token.lock().unwrap().value)
+                .parse()
+                .unwrap(),
+        );
+
+        Ok(request)
+    }
+}
+
+/// Generates an auth challenge then generates and returns validated auth tokens.
+pub async fn generate_auth_tokens(
+    auth_service_client: &mut AuthServiceClient<Channel>,
+    // used to sign challenges
+    keypair: &Keypair,
+) -> crate::proxy::Result<(
+    Token, /* access_token */
+    Token, /* refresh_token */
+)> {
+    debug!("generate_auth_challenge");
+    let challenge_response = auth_service_client
+        .generate_auth_challenge(GenerateAuthChallengeRequest {
+            role: Role::Validator as i32,
+            pubkey: keypair.pubkey().as_ref().to_vec(),
+        })
+        .await
+        .map_err(|e: Status| {
+            if e.code() == Code::PermissionDenied {
+                ProxyError::AuthenticationPermissionDenied
+            } else {
+                ProxyError::AuthenticationError(e.to_string())
+            }
+        })?;
+
+    let formatted_challenge = format!(
+        "{}-{}",
+        keypair.pubkey(),
+        challenge_response.into_inner().challenge
+    );
+
+    let signed_challenge = keypair
+        .sign_message(formatted_challenge.as_bytes())
+        .as_ref()
+        .to_vec();
+
+    debug!(
+        "formatted_challenge: {} signed_challenge: {:?}",
+        formatted_challenge, signed_challenge
+    );
+
+    debug!("generate_auth_tokens");
+    let auth_tokens = auth_service_client
+        .generate_auth_tokens(GenerateAuthTokensRequest {
+            challenge: formatted_challenge,
+            client_pubkey: keypair.pubkey().as_ref().to_vec(),
+            signed_challenge,
+        })
+        .await
+        .map_err(|e| ProxyError::AuthenticationError(e.to_string()))?;
+
+    let inner = auth_tokens.into_inner();
+    let access_token = get_validated_token(inner.access_token)?;
+    let refresh_token = get_validated_token(inner.refresh_token)?;
+
+    Ok((access_token, refresh_token))
+}
+
+/// Tries to refresh the access token or run full-reauth if needed.
+pub async fn maybe_refresh_auth_tokens(
+    auth_service_client: &mut AuthServiceClient<Channel>,
+    access_token: &Arc<Mutex<Token>>,
+    refresh_token: &Token,
+    cluster_info: &Arc<ClusterInfo>,
+    connection_timeout: &Duration,
+    refresh_within_s: u64,
+) -> crate::proxy::Result<(
+    Option<Token>, // access token
+    Option<Token>, // refresh token
+)> {
+    let access_token_expiry: u64 = access_token
+        .lock()
+        .unwrap()
+        .expires_at_utc
+        .as_ref()
+        .map(|ts| ts.seconds as u64)
+        .unwrap_or_default();
+    let refresh_token_expiry: u64 = refresh_token
+        .expires_at_utc
+        .as_ref()
+        .map(|ts| ts.seconds as u64)
+        .unwrap_or_default();
+
+    let now = Utc::now().timestamp() as u64;
+
+    let should_refresh_access =
+        access_token_expiry.checked_sub(now).unwrap_or_default() <= refresh_within_s;
+    let should_generate_new_tokens =
+        refresh_token_expiry.checked_sub(now).unwrap_or_default() <= refresh_within_s;
+
+    if should_generate_new_tokens {
+        let kp = cluster_info.keypair().clone();
+
+        let (new_access_token, new_refresh_token) = timeout(
+            *connection_timeout,
+            generate_auth_tokens(auth_service_client, kp.as_ref()),
+        )
+        .await
+        .map_err(|_| ProxyError::MethodTimeout("generate_auth_tokens".to_string()))?
+        .map_err(|e| ProxyError::MethodError(e.to_string()))?;
+
+        return Ok((Some(new_access_token), Some(new_refresh_token)));
+    } else if should_refresh_access {
+        let new_access_token = timeout(
+            *connection_timeout,
+            refresh_access_token(auth_service_client, refresh_token),
+        )
+        .await
+        .map_err(|_| ProxyError::MethodTimeout("refresh_access_token".to_string()))?
+        .map_err(|e| ProxyError::MethodError(e.to_string()))?;
+
+        return Ok((Some(new_access_token), None));
+    }
+
+    Ok((None, None))
+}
+
+pub async fn refresh_access_token(
+    auth_service_client: &mut AuthServiceClient<Channel>,
+    refresh_token: &Token,
+) -> crate::proxy::Result<Token> {
+    let response = auth_service_client
+        .refresh_access_token(RefreshAccessTokenRequest {
+            refresh_token: refresh_token.value.clone(),
+        })
+        .await
+        .map_err(|e| ProxyError::AuthenticationError(e.to_string()))?;
+    get_validated_token(response.into_inner().access_token)
+}
+
+/// An invalid token is one where any of its fields are None or the token itself is None.
+/// Performs the necessary validations on the auth tokens before returning,
+/// i.e. it is safe to call .unwrap() on the token fields from the call-site.
+fn get_validated_token(maybe_token: Option<Token>) -> crate::proxy::Result<Token> {
+    let token = maybe_token
+        .ok_or_else(|| ProxyError::BadAuthenticationToken("received a null token".to_string()))?;
+    if token.expires_at_utc.is_none() {
+        Err(ProxyError::BadAuthenticationToken(
+            "expires_at_utc field is null".to_string(),
+        ))
+    } else {
+        Ok(token)
+    }
+}
diff --git a/core/src/proxy/block_engine_stage.rs b/core/src/proxy/block_engine_stage.rs
new file mode 100644
index 0000000000..4128f5379f
--- /dev/null
+++ b/core/src/proxy/block_engine_stage.rs
@@ -0,0 +1,533 @@
+//! Maintains a connection to the Block Engine.
+//!
+//! The Block Engine is responsible for the following:
+//! - Acts as a system that sends high profit bundles and transactions to a validator.
+//! - Sends transactions and bundles to the validator.
+use {
+    crate::{
+        banking_trace::BankingPacketSender,
+        packet_bundle::PacketBundle,
+        proto_packet_to_packet,
+        proxy::{
+            auth::{generate_auth_tokens, maybe_refresh_auth_tokens, AuthInterceptor},
+            ProxyError,
+        },
+    },
+    crossbeam_channel::Sender,
+    jito_protos::proto::{
+        auth::{auth_service_client::AuthServiceClient, Token},
+        block_engine::{
+            self, block_engine_validator_client::BlockEngineValidatorClient,
+            BlockBuilderFeeInfoRequest,
+        },
+    },
+    solana_gossip::cluster_info::ClusterInfo,
+    solana_perf::packet::PacketBatch,
+    solana_sdk::{
+        pubkey::Pubkey, saturating_add_assign, signature::Signer, signer::keypair::Keypair,
+    },
+    std::{
+        str::FromStr,
+        sync::{
+            atomic::{AtomicBool, Ordering},
+            Arc, Mutex,
+        },
+        thread::{self, Builder, JoinHandle},
+        time::Duration,
+    },
+    tokio::time::{interval, sleep, timeout},
+    tonic::{
+        codegen::InterceptedService,
+        transport::{Channel, Endpoint},
+        Status, Streaming,
+    },
+};
+
+const CONNECTION_TIMEOUT_S: u64 = 10;
+const CONNECTION_BACKOFF_S: u64 = 5;
+
+#[derive(Default)]
+struct BlockEngineStageStats {
+    num_bundles: u64,
+    num_bundle_packets: u64,
+    num_packets: u64,
+    num_empty_packets: u64,
+}
+
+impl BlockEngineStageStats {
+    pub(crate) fn report(&self) {
+        datapoint_info!(
+            "block_engine_stage-stats",
+            ("num_bundles", self.num_bundles, i64),
+            ("num_bundle_packets", self.num_bundle_packets, i64),
+            ("num_packets", self.num_packets, i64),
+            ("num_empty_packets", self.num_empty_packets, i64)
+        );
+    }
+}
+
+pub struct BlockBuilderFeeInfo {
+    pub block_builder: Pubkey,
+    pub block_builder_commission: u64,
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct BlockEngineConfig {
+    /// Block Engine URL
+    pub block_engine_url: String,
+
+    /// If set then it will be assumed the backend verified packets so signature verification will be bypassed in the validator.
+    pub trust_packets: bool,
+}
+
+pub struct BlockEngineStage {
+    t_hdls: Vec<JoinHandle<()>>,
+}
+
+impl BlockEngineStage {
+    pub fn new(
+        block_engine_config: Arc<Mutex<BlockEngineConfig>>,
+        // Channel that bundles get piped through.
+        bundle_tx: Sender<Vec<PacketBundle>>,
+        // The keypair stored here is used to sign auth challenges.
+        cluster_info: Arc<ClusterInfo>,
+        // Channel that non-trusted packets get piped through.
+        packet_tx: Sender<PacketBatch>,
+        // Channel that trusted packets get piped through.
+        banking_packet_sender: BankingPacketSender,
+        exit: Arc<AtomicBool>,
+        block_builder_fee_info: &Arc<Mutex<BlockBuilderFeeInfo>>,
+    ) -> Self {
+        let block_builder_fee_info = block_builder_fee_info.clone();
+
+        let thread = Builder::new()
+            .name("block-engine-stage".to_string())
+            .spawn(move || {
+                let rt = tokio::runtime::Builder::new_multi_thread()
+                    .enable_all()
+                    .build()
+                    .unwrap();
+                rt.block_on(Self::start(
+                    block_engine_config,
+                    cluster_info,
+                    bundle_tx,
+                    packet_tx,
+                    banking_packet_sender,
+                    exit,
+                    block_builder_fee_info,
+                ));
+            })
+            .unwrap();
+
+        Self {
+            t_hdls: vec![thread],
+        }
+    }
+
+    pub fn join(self) -> thread::Result<()> {
+        for t in self.t_hdls {
+            t.join()?;
+        }
+        Ok(())
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    async fn start(
+        block_engine_config: Arc<Mutex<BlockEngineConfig>>,
+        cluster_info: Arc<ClusterInfo>,
+        bundle_tx: Sender<Vec<PacketBundle>>,
+        packet_tx: Sender<PacketBatch>,
+        banking_packet_sender: BankingPacketSender,
+        exit: Arc<AtomicBool>,
+        block_builder_fee_info: Arc<Mutex<BlockBuilderFeeInfo>>,
+    ) {
+        const CONNECTION_TIMEOUT: Duration = Duration::from_secs(CONNECTION_TIMEOUT_S);
+        const CONNECTION_BACKOFF: Duration = Duration::from_secs(CONNECTION_BACKOFF_S);
+        let mut error_count: u64 = 0;
+
+        while !exit.load(Ordering::Relaxed) {
+            // Wait until a valid config is supplied (either initially or by admin rpc)
+            // Use if!/else here to avoid extra CONNECTION_BACKOFF wait on successful termination
+            if !Self::is_valid_block_engine_config(&block_engine_config.lock().unwrap()) {
+                sleep(CONNECTION_BACKOFF).await;
+            } else if let Err(e) = Self::connect_auth_and_stream(
+                &block_engine_config,
+                &cluster_info,
+                &bundle_tx,
+                &packet_tx,
+                &banking_packet_sender,
+                &exit,
+                &block_builder_fee_info,
+                &CONNECTION_TIMEOUT,
+            )
+            .await
+            {
+                match e {
+                    // This error is frequent on hot spares, and the parsed string does not work
+                    // with datapoints (incorrect escaping).
+                    ProxyError::AuthenticationPermissionDenied => {
+                        warn!("block engine permission denied. not on leader schedule. ignore if hot-spare.")
+                    }
+                    e => {
+                        error_count += 1;
+                        datapoint_warn!(
+                            "block_engine_stage-proxy_error",
+                            ("count", error_count, i64),
+                            ("error", e.to_string(), String),
+                        );
+                    }
+                }
+                sleep(CONNECTION_BACKOFF).await;
+            }
+        }
+    }
+
+    async fn connect_auth_and_stream(
+        block_engine_config: &Arc<Mutex<BlockEngineConfig>>,
+        cluster_info: &Arc<ClusterInfo>,
+        bundle_tx: &Sender<Vec<PacketBundle>>,
+        packet_tx: &Sender<PacketBatch>,
+        banking_packet_sender: &BankingPacketSender,
+        exit: &Arc<AtomicBool>,
+        block_builder_fee_info: &Arc<Mutex<BlockBuilderFeeInfo>>,
+        connection_timeout: &Duration,
+    ) -> crate::proxy::Result<()> {
+        // Get a copy of configs here in case they have changed at runtime
+        let keypair = cluster_info.keypair().clone();
+        let local_config = block_engine_config.lock().unwrap().clone();
+
+        let mut backend_endpoint = Endpoint::from_shared(local_config.block_engine_url.clone())
+            .map_err(|_| {
+                ProxyError::BlockEngineConnectionError(format!(
+                    "invalid block engine url value: {}",
+                    local_config.block_engine_url
+                ))
+            })?
+            .tcp_keepalive(Some(Duration::from_secs(60)));
+        if local_config.block_engine_url.starts_with("https") {
+            backend_endpoint = backend_endpoint
+                .tls_config(tonic::transport::ClientTlsConfig::new())
+                .map_err(|_| {
+                    ProxyError::BlockEngineConnectionError(
+                        "failed to set tls_config for block engine service".to_string(),
+                    )
+                })?;
+        }
+
+        debug!("connecting to auth: {}", local_config.block_engine_url);
+        let auth_channel = timeout(*connection_timeout, backend_endpoint.connect())
+            .await
+            .map_err(|_| ProxyError::AuthenticationConnectionTimeout)?
+            .map_err(|e| ProxyError::AuthenticationConnectionError(e.to_string()))?;
+
+        let mut auth_client = AuthServiceClient::new(auth_channel);
+
+        debug!("generating authentication token");
+        let (access_token, refresh_token) = timeout(
+            *connection_timeout,
+            generate_auth_tokens(&mut auth_client, &keypair),
+        )
+        .await
+        .map_err(|_| ProxyError::AuthenticationTimeout)??;
+
+        datapoint_info!(
+            "block_engine_stage-tokens_generated",
+            ("url", local_config.block_engine_url, String),
+            ("count", 1, i64),
+        );
+
+        debug!(
+            "connecting to block engine: {}",
+            local_config.block_engine_url
+        );
+        let block_engine_channel = timeout(*connection_timeout, backend_endpoint.connect())
+            .await
+            .map_err(|_| ProxyError::BlockEngineConnectionTimeout)?
+            .map_err(|e| ProxyError::BlockEngineConnectionError(e.to_string()))?;
+
+        let access_token = Arc::new(Mutex::new(access_token));
+        let block_engine_client = BlockEngineValidatorClient::with_interceptor(
+            block_engine_channel,
+            AuthInterceptor::new(access_token.clone()),
+        );
+
+        Self::start_consuming_block_engine_bundles_and_packets(
+            bundle_tx,
+            block_engine_client,
+            packet_tx,
+            &local_config,
+            block_engine_config,
+            banking_packet_sender,
+            exit,
+            block_builder_fee_info,
+            auth_client,
+            access_token,
+            refresh_token,
+            connection_timeout,
+            keypair,
+            cluster_info,
+        )
+        .await
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    async fn start_consuming_block_engine_bundles_and_packets(
+        bundle_tx: &Sender<Vec<PacketBundle>>,
+        mut client: BlockEngineValidatorClient<InterceptedService<Channel, AuthInterceptor>>,
+        packet_tx: &Sender<PacketBatch>,
+        local_config: &BlockEngineConfig, // local copy of config with current connections
+        global_config: &Arc<Mutex<BlockEngineConfig>>, // guarded reference for detecting run-time updates
+        banking_packet_sender: &BankingPacketSender,
+        exit: &Arc<AtomicBool>,
+        block_builder_fee_info: &Arc<Mutex<BlockBuilderFeeInfo>>,
+        auth_client: AuthServiceClient<Channel>,
+        access_token: Arc<Mutex<Token>>,
+        refresh_token: Token,
+        connection_timeout: &Duration,
+        keypair: Arc<Keypair>,
+        cluster_info: &Arc<ClusterInfo>,
+    ) -> crate::proxy::Result<()> {
+        let subscribe_packets_stream = timeout(
+            *connection_timeout,
+            client.subscribe_packets(block_engine::SubscribePacketsRequest {}),
+        )
+        .await
+        .map_err(|_| ProxyError::MethodTimeout("block_engine_subscribe_packets".to_string()))?
+        .map_err(|e| ProxyError::MethodError(e.to_string()))?
+        .into_inner();
+
+        let subscribe_bundles_stream = timeout(
+            *connection_timeout,
+            client.subscribe_bundles(block_engine::SubscribeBundlesRequest {}),
+        )
+        .await
+        .map_err(|_| ProxyError::MethodTimeout("subscribe_bundles".to_string()))?
+        .map_err(|e| ProxyError::MethodError(e.to_string()))?
+        .into_inner();
+
+        let block_builder_info = timeout(
+            *connection_timeout,
+            client.get_block_builder_fee_info(BlockBuilderFeeInfoRequest {}),
+        )
+        .await
+        .map_err(|_| ProxyError::MethodTimeout("get_block_builder_fee_info".to_string()))?
+        .map_err(|e| ProxyError::MethodError(e.to_string()))?
+        .into_inner();
+
+        {
+            let mut bb_fee = block_builder_fee_info.lock().unwrap();
+            bb_fee.block_builder_commission = block_builder_info.commission;
+            bb_fee.block_builder =
+                Pubkey::from_str(&block_builder_info.pubkey).unwrap_or(bb_fee.block_builder);
+        }
+
+        Self::consume_bundle_and_packet_stream(
+            client,
+            (subscribe_bundles_stream, subscribe_packets_stream),
+            bundle_tx,
+            packet_tx,
+            local_config,
+            global_config,
+            banking_packet_sender,
+            exit,
+            block_builder_fee_info,
+            auth_client,
+            access_token,
+            refresh_token,
+            keypair,
+            cluster_info,
+            connection_timeout,
+        )
+        .await
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    async fn consume_bundle_and_packet_stream(
+        mut client: BlockEngineValidatorClient<InterceptedService<Channel, AuthInterceptor>>,
+        (mut bundle_stream, mut packet_stream): (
+            Streaming<block_engine::SubscribeBundlesResponse>,
+            Streaming<block_engine::SubscribePacketsResponse>,
+        ),
+        bundle_tx: &Sender<Vec<PacketBundle>>,
+        packet_tx: &Sender<PacketBatch>,
+        local_config: &BlockEngineConfig, // local copy of config with current connections
+        global_config: &Arc<Mutex<BlockEngineConfig>>, // guarded reference for detecting run-time updates
+        banking_packet_sender: &BankingPacketSender,
+        exit: &Arc<AtomicBool>,
+        block_builder_fee_info: &Arc<Mutex<BlockBuilderFeeInfo>>,
+        mut auth_client: AuthServiceClient<Channel>,
+        access_token: Arc<Mutex<Token>>,
+        mut refresh_token: Token,
+        keypair: Arc<Keypair>,
+        cluster_info: &Arc<ClusterInfo>,
+        connection_timeout: &Duration,
+    ) -> crate::proxy::Result<()> {
+        const METRICS_TICK: Duration = Duration::from_secs(1);
+        const MAINTENANCE_TICK: Duration = Duration::from_secs(10 * 60);
+        let refresh_within_s: u64 = METRICS_TICK.as_secs().saturating_mul(3).saturating_div(2);
+
+        let mut num_full_refreshes: u64 = 1;
+        let mut num_refresh_access_token: u64 = 0;
+        let mut block_engine_stats = BlockEngineStageStats::default();
+        let mut metrics_and_auth_tick = interval(METRICS_TICK);
+        let mut maintenance_tick = interval(MAINTENANCE_TICK);
+
+        info!("connected to packet and bundle stream");
+
+        while !exit.load(Ordering::Relaxed) {
+            tokio::select! {
+                maybe_msg = packet_stream.message() => {
+                    let resp = maybe_msg?.ok_or(ProxyError::GrpcStreamDisconnected)?;
+                    Self::handle_block_engine_packets(resp, packet_tx, banking_packet_sender, local_config.trust_packets, &mut block_engine_stats)?;
+                }
+                maybe_bundles = bundle_stream.message() => {
+                    Self::handle_block_engine_maybe_bundles(maybe_bundles, bundle_tx, &mut block_engine_stats)?;
+                }
+                _ = metrics_and_auth_tick.tick() => {
+                    block_engine_stats.report();
+                    block_engine_stats = BlockEngineStageStats::default();
+
+                    if cluster_info.id() != keypair.pubkey() {
+                        return Err(ProxyError::AuthenticationConnectionError("validator identity changed".to_string()));
+                    }
+
+                    if *global_config.lock().unwrap() != *local_config {
+                        return Err(ProxyError::AuthenticationConnectionError("block engine config changed".to_string()));
+                    }
+
+                    let (maybe_new_access, maybe_new_refresh) = maybe_refresh_auth_tokens(&mut auth_client,
+                        &access_token,
+                        &refresh_token,
+                        cluster_info,
+                        connection_timeout,
+                        refresh_within_s,
+                    ).await?;
+
+                    if let Some(new_token) = maybe_new_access {
+                        num_refresh_access_token += 1;
+                        datapoint_info!(
+                            "block_engine_stage-refresh_access_token",
+                            ("url", &local_config.block_engine_url, String),
+                            ("count", num_refresh_access_token, i64),
+                        );
+                        *access_token.lock().unwrap() = new_token;
+                    }
+                    if let Some(new_token) = maybe_new_refresh {
+                        num_full_refreshes += 1;
+                        datapoint_info!(
+                            "block_engine_stage-tokens_generated",
+                            ("url", &local_config.block_engine_url, String),
+                            ("count", num_full_refreshes, i64),
+                        );
+                        refresh_token = new_token;
+                    }
+                }
+                _ = maintenance_tick.tick() => {
+                    let block_builder_info = timeout(
+                        *connection_timeout,
+                        client.get_block_builder_fee_info(BlockBuilderFeeInfoRequest{})
+                    )
+                    .await
+                    .map_err(|_| ProxyError::MethodTimeout("get_block_builder_fee_info".to_string()))?
+                    .map_err(|e| ProxyError::MethodError(e.to_string()))?
+                    .into_inner();
+
+                    let mut bb_fee = block_builder_fee_info.lock().unwrap();
+                    bb_fee.block_builder_commission = block_builder_info.commission;
+                    bb_fee.block_builder = Pubkey::from_str(&block_builder_info.pubkey).unwrap_or(bb_fee.block_builder);
+                }
+            }
+        }
+        Ok(())
+    }
+
+    fn handle_block_engine_maybe_bundles(
+        maybe_bundles_response: Result<Option<block_engine::SubscribeBundlesResponse>, Status>,
+        bundle_sender: &Sender<Vec<PacketBundle>>,
+        block_engine_stats: &mut BlockEngineStageStats,
+    ) -> crate::proxy::Result<()> {
+        let bundles_response = maybe_bundles_response?.ok_or(ProxyError::GrpcStreamDisconnected)?;
+        let bundles: Vec<PacketBundle> = bundles_response
+            .bundles
+            .into_iter()
+            .filter_map(|bundle| {
+                Some(PacketBundle {
+                    batch: PacketBatch::new(
+                        bundle
+                            .bundle?
+                            .packets
+                            .into_iter()
+                            .map(proto_packet_to_packet)
+                            .collect(),
+                    ),
+                    bundle_id: bundle.uuid,
+                })
+            })
+            .collect();
+
+        saturating_add_assign!(block_engine_stats.num_bundles, bundles.len() as u64);
+        saturating_add_assign!(
+            block_engine_stats.num_bundle_packets,
+            bundles.iter().map(|bundle| bundle.batch.len() as u64).sum()
+        );
+
+        // NOTE: bundles are sanitized in bundle_sanitizer module
+        bundle_sender
+            .send(bundles)
+            .map_err(|_| ProxyError::PacketForwardError)
+    }
+
+    fn handle_block_engine_packets(
+        resp: block_engine::SubscribePacketsResponse,
+        packet_tx: &Sender<PacketBatch>,
+        banking_packet_sender: &BankingPacketSender,
+        trust_packets: bool,
+        block_engine_stats: &mut BlockEngineStageStats,
+    ) -> crate::proxy::Result<()> {
+        if let Some(batch) = resp.batch {
+            if batch.packets.is_empty() {
+                saturating_add_assign!(block_engine_stats.num_empty_packets, 1);
+                return Ok(());
+            }
+
+            let packet_batch = PacketBatch::new(
+                batch
+                    .packets
+                    .into_iter()
+                    .map(proto_packet_to_packet)
+                    .collect(),
+            );
+
+            saturating_add_assign!(block_engine_stats.num_packets, packet_batch.len() as u64);
+
+            if trust_packets {
+                banking_packet_sender
+                    .send(Arc::new((vec![packet_batch], None)))
+                    .map_err(|_| ProxyError::PacketForwardError)?;
+            } else {
+                packet_tx
+                    .send(packet_batch)
+                    .map_err(|_| ProxyError::PacketForwardError)?;
+            }
+        } else {
+            saturating_add_assign!(block_engine_stats.num_empty_packets, 1);
+        }
+
+        Ok(())
+    }
+
+    pub fn is_valid_block_engine_config(config: &BlockEngineConfig) -> bool {
+        if config.block_engine_url.is_empty() {
+            warn!("can't connect to block_engine. missing block_engine_url.");
+            return false;
+        }
+        if let Err(e) = Endpoint::from_str(&config.block_engine_url) {
+            error!(
+                "can't connect to block engine. error creating block engine endpoint - {}",
+                e.to_string()
+            );
+            return false;
+        }
+        true
+    }
+}
diff --git a/core/src/proxy/fetch_stage_manager.rs b/core/src/proxy/fetch_stage_manager.rs
new file mode 100644
index 0000000000..38471fc512
--- /dev/null
+++ b/core/src/proxy/fetch_stage_manager.rs
@@ -0,0 +1,170 @@
+use {
+    crate::proxy::{HeartbeatEvent, ProxyError},
+    crossbeam_channel::{select, tick, Receiver, Sender},
+    solana_client::connection_cache::Protocol,
+    solana_gossip::{cluster_info::ClusterInfo, contact_info},
+    solana_perf::packet::PacketBatch,
+    std::{
+        net::SocketAddr,
+        sync::{
+            atomic::{AtomicBool, Ordering},
+            Arc,
+        },
+        thread::{self, Builder, JoinHandle},
+        time::{Duration, Instant},
+    },
+};
+
+const HEARTBEAT_TIMEOUT: Duration = Duration::from_millis(1500); // Empirically determined from load testing
+const DISCONNECT_DELAY: Duration = Duration::from_secs(60);
+const METRICS_CADENCE: Duration = Duration::from_secs(1);
+
+/// Manages switching between the validator's tpu ports and that of the proxy's.
+/// Switch-overs are triggered by late and missed heartbeats.    
+pub struct FetchStageManager {
+    t_hdl: JoinHandle<()>,
+}
+
+impl FetchStageManager {
+    pub fn new(
+        // ClusterInfo is used to switch between advertising the proxy's TPU ports and that of this validator's.
+        cluster_info: Arc<ClusterInfo>,
+        // Channel that heartbeats are received from. Entirely responsible for triggering switch-overs.
+        heartbeat_rx: Receiver<HeartbeatEvent>,
+        // Channel that packets from FetchStage are intercepted from.
+        packet_intercept_rx: Receiver<PacketBatch>,
+        // Intercepted packets get piped through here.
+        packet_tx: Sender<PacketBatch>,
+        exit: Arc<AtomicBool>,
+    ) -> Self {
+        let t_hdl = Self::start(
+            cluster_info,
+            heartbeat_rx,
+            packet_intercept_rx,
+            packet_tx,
+            exit,
+        );
+
+        Self { t_hdl }
+    }
+
+    /// Disconnect fetch behaviour
+    /// Starts connected
+    /// When connected and a packet is received, forward it
+    /// When disconnected, packet is dropped
+    /// When receiving heartbeat while connected and not pending disconnect
+    ///      Sets pending_disconnect to true and records time
+    /// When receiving heartbeat while connected, and pending for > DISCONNECT_DELAY_SEC
+    ///      Sets fetch_connected to false, pending_disconnect to false
+    ///      Advertises TPU ports sent in heartbeat
+    /// When tick is received without heartbeat_received
+    ///      Sets fetch_connected to true, pending_disconnect to false
+    ///      Advertises saved contact info
+    fn start(
+        cluster_info: Arc<ClusterInfo>,
+        heartbeat_rx: Receiver<HeartbeatEvent>,
+        packet_intercept_rx: Receiver<PacketBatch>,
+        packet_tx: Sender<PacketBatch>,
+        exit: Arc<AtomicBool>,
+    ) -> JoinHandle<()> {
+        Builder::new().name("fetch-stage-manager".into()).spawn(move || {
+            let my_fallback_contact_info = cluster_info.my_contact_info();
+
+            let mut fetch_connected = true;
+            let mut heartbeat_received = false;
+            let mut pending_disconnect = false;
+
+            let mut pending_disconnect_ts = Instant::now();
+
+            let heartbeat_tick = tick(HEARTBEAT_TIMEOUT);
+            let metrics_tick = tick(METRICS_CADENCE);
+            let mut packets_forwarded = 0;
+            let mut heartbeats_received = 0;
+            loop {
+                select! {
+                    recv(packet_intercept_rx) -> pkt => {
+                        match pkt {
+                            Ok(pkt) => {
+                                if fetch_connected {
+                                    if packet_tx.send(pkt).is_err() {
+                                        error!("{:?}", ProxyError::PacketForwardError);
+                                        return;
+                                    }
+                                    packets_forwarded += 1;
+                                }
+                            }
+                            Err(_) => {
+                                warn!("packet intercept receiver disconnected, shutting down");
+                                return;
+                            }
+                        }
+                    }
+                    recv(heartbeat_tick) -> _ => {
+                        if exit.load(Ordering::Relaxed) {
+                            break;
+                        }
+                        if !heartbeat_received && (!fetch_connected || pending_disconnect) {
+                            warn!("heartbeat late, reconnecting fetch stage");
+                            fetch_connected = true;
+                            pending_disconnect = false;
+
+                            // unwrap safe here bc contact_info.tpu(Protocol::QUIC) and contact_info.tpu_forwards(Protocol::QUIC)
+                            // are checked on startup
+                            if let Err(e) = Self::set_tpu_addresses(&cluster_info, my_fallback_contact_info.tpu(Protocol::QUIC).unwrap(), my_fallback_contact_info.tpu_forwards(Protocol::QUIC).unwrap()) {
+                                error!("error setting tpu or tpu_fwd to ({:?}, {:?}), error: {:?}", my_fallback_contact_info.tpu(Protocol::QUIC).unwrap(), my_fallback_contact_info.tpu_forwards(Protocol::QUIC).unwrap(), e);
+                            }
+                            heartbeats_received = 0;
+                        }
+                        heartbeat_received = false;
+                    }
+                    recv(heartbeat_rx) -> tpu_info => {
+                        if let Ok((tpu_addr, tpu_forward_addr)) = tpu_info {
+                            heartbeats_received += 1;
+                            heartbeat_received = true;
+                            if fetch_connected && !pending_disconnect {
+                                info!("received heartbeat while fetch stage connected, pending disconnect after delay");
+                                pending_disconnect_ts = Instant::now();
+                                pending_disconnect = true;
+                            }
+                            if fetch_connected && pending_disconnect && pending_disconnect_ts.elapsed() > DISCONNECT_DELAY {
+                                info!("disconnecting fetch stage");
+                                fetch_connected = false;
+                                pending_disconnect = false;
+                                if let Err(e) = Self::set_tpu_addresses(&cluster_info, tpu_addr, tpu_forward_addr) {
+                                    error!("error setting tpu or tpu_fwd to ({:?}, {:?}), error: {:?}", tpu_addr, tpu_forward_addr, e);
+                                }
+                            }
+                        } else {
+                            {
+                                warn!("relayer heartbeat receiver disconnected, shutting down");
+                                return;
+                            }
+                        }
+                    }
+                    recv(metrics_tick) -> _ => {
+                        datapoint_info!(
+                            "relayer-heartbeat",
+                            ("fetch_stage_packets_forwarded", packets_forwarded, i64),
+                            ("heartbeats_received", heartbeats_received, i64),
+                        );
+
+                    }
+                }
+            }
+        }).unwrap()
+    }
+
+    fn set_tpu_addresses(
+        cluster_info: &Arc<ClusterInfo>,
+        tpu_address: SocketAddr,
+        tpu_forward_address: SocketAddr,
+    ) -> Result<(), contact_info::Error> {
+        cluster_info.set_tpu(tpu_address)?;
+        cluster_info.set_tpu_forwards(tpu_forward_address)?;
+        Ok(())
+    }
+
+    pub fn join(self) -> thread::Result<()> {
+        self.t_hdl.join()
+    }
+}
diff --git a/core/src/proxy/mod.rs b/core/src/proxy/mod.rs
new file mode 100644
index 0000000000..86d48482aa
--- /dev/null
+++ b/core/src/proxy/mod.rs
@@ -0,0 +1,100 @@
+//! This module contains logic for connecting to an external Relayer and Block Engine.
+//! The Relayer acts as an external TPU and TPU Forward socket while the Block Engine
+//! is tasked with streaming high value bundles to the validator. The validator can run
+//! in one of 3 modes:
+//!     1. Connected to Relayer and Block Engine.
+//!         - This is the ideal mode as it increases the probability of building the most profitable blocks.
+//!     2. Connected only to Relayer.
+//!         - A validator may choose to run in this mode if the main concern is to offload ingress traffic deduplication and sig-verification.
+//!     3. Connected only to Block Engine.
+//!         - Running in this mode means pending transactions are not exposed to external actors. This mode is ideal if the validator wishes
+//!           to accept bundles while maintaining some level of privacy for in-flight transactions.
+
+mod auth;
+pub mod block_engine_stage;
+pub mod fetch_stage_manager;
+pub mod relayer_stage;
+
+use {
+    std::{
+        net::{AddrParseError, SocketAddr},
+        result,
+    },
+    thiserror::Error,
+    tonic::Status,
+};
+
+type Result<T> = result::Result<T, ProxyError>;
+type HeartbeatEvent = (SocketAddr, SocketAddr);
+
+#[derive(Error, Debug)]
+pub enum ProxyError {
+    #[error("grpc error: {0}")]
+    GrpcError(#[from] Status),
+
+    #[error("stream disconnected")]
+    GrpcStreamDisconnected,
+
+    #[error("heartbeat error")]
+    HeartbeatChannelError,
+
+    #[error("heartbeat expired")]
+    HeartbeatExpired,
+
+    #[error("error forwarding packet to banking stage")]
+    PacketForwardError,
+
+    #[error("missing tpu config: {0:?}")]
+    MissingTpuSocket(String),
+
+    #[error("invalid socket address: {0:?}")]
+    InvalidSocketAddress(#[from] AddrParseError),
+
+    #[error("invalid gRPC data: {0:?}")]
+    InvalidData(String),
+
+    #[error("timeout: {0:?}")]
+    ConnectionError(#[from] tonic::transport::Error),
+
+    #[error("AuthenticationConnectionTimeout")]
+    AuthenticationConnectionTimeout,
+
+    #[error("AuthenticationTimeout")]
+    AuthenticationTimeout,
+
+    #[error("AuthenticationConnectionError: {0:?}")]
+    AuthenticationConnectionError(String),
+
+    #[error("BlockEngineConnectionTimeout")]
+    BlockEngineConnectionTimeout,
+
+    #[error("BlockEngineTimeout")]
+    BlockEngineTimeout,
+
+    #[error("BlockEngineConnectionError: {0:?}")]
+    BlockEngineConnectionError(String),
+
+    #[error("RelayerConnectionTimeout")]
+    RelayerConnectionTimeout,
+
+    #[error("RelayerTimeout")]
+    RelayerEngineTimeout,
+
+    #[error("RelayerConnectionError: {0:?}")]
+    RelayerConnectionError(String),
+
+    #[error("AuthenticationError: {0:?}")]
+    AuthenticationError(String),
+
+    #[error("AuthenticationPermissionDenied")]
+    AuthenticationPermissionDenied,
+
+    #[error("BadAuthenticationToken: {0:?}")]
+    BadAuthenticationToken(String),
+
+    #[error("MethodTimeout: {0:?}")]
+    MethodTimeout(String),
+
+    #[error("MethodError: {0:?}")]
+    MethodError(String),
+}
diff --git a/core/src/proxy/relayer_stage.rs b/core/src/proxy/relayer_stage.rs
new file mode 100644
index 0000000000..3c754fb9e4
--- /dev/null
+++ b/core/src/proxy/relayer_stage.rs
@@ -0,0 +1,495 @@
+//! Maintains a connection to the Relayer.
+//!
+//! The external Relayer is responsible for the following:
+//! - Acts as a TPU proxy.
+//! - Sends transactions to the validator.
+//! - Does not bundles to avoid DOS vector.
+//! - When validator connects, it changes its TPU and TPU forward address to the relayer.
+//! - Expected to send heartbeat to validator as watchdog. If watchdog times out, the validator
+//!   disconnects and reverts the TPU and TPU forward settings.
+
+use {
+    crate::{
+        banking_trace::BankingPacketSender,
+        proto_packet_to_packet,
+        proxy::{
+            auth::{generate_auth_tokens, maybe_refresh_auth_tokens, AuthInterceptor},
+            HeartbeatEvent, ProxyError,
+        },
+    },
+    crossbeam_channel::Sender,
+    jito_protos::proto::{
+        auth::{auth_service_client::AuthServiceClient, Token},
+        relayer::{self, relayer_client::RelayerClient},
+    },
+    solana_gossip::cluster_info::ClusterInfo,
+    solana_perf::packet::PacketBatch,
+    solana_sdk::{
+        saturating_add_assign,
+        signature::{Keypair, Signer},
+    },
+    std::{
+        net::{IpAddr, Ipv4Addr, SocketAddr},
+        str::FromStr,
+        sync::{
+            atomic::{AtomicBool, Ordering},
+            Arc, Mutex,
+        },
+        thread::{self, Builder, JoinHandle},
+        time::{Duration, Instant},
+    },
+    tokio::time::{interval, sleep, timeout},
+    tonic::{
+        codegen::InterceptedService,
+        transport::{Channel, Endpoint},
+        Streaming,
+    },
+};
+
+const CONNECTION_TIMEOUT_S: u64 = 10;
+const CONNECTION_BACKOFF_S: u64 = 5;
+
+#[derive(Default)]
+struct RelayerStageStats {
+    num_empty_messages: u64,
+    num_packets: u64,
+    num_heartbeats: u64,
+}
+
+impl RelayerStageStats {
+    pub(crate) fn report(&self) {
+        datapoint_info!(
+            "relayer_stage-stats",
+            ("num_empty_messages", self.num_empty_messages, i64),
+            ("num_packets", self.num_packets, i64),
+            ("num_heartbeats", self.num_heartbeats, i64),
+        );
+    }
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct RelayerConfig {
+    /// Relayer URL
+    pub relayer_url: String,
+
+    /// Interval at which heartbeats are expected.
+    pub expected_heartbeat_interval: Duration,
+
+    /// The max tolerable age of the last heartbeat.
+    pub oldest_allowed_heartbeat: Duration,
+
+    /// If set then it will be assumed the backend verified packets so signature verification will be bypassed in the validator.
+    pub trust_packets: bool,
+}
+
+pub struct RelayerStage {
+    t_hdls: Vec<JoinHandle<()>>,
+}
+
+impl RelayerStage {
+    pub fn new(
+        relayer_config: Arc<Mutex<RelayerConfig>>,
+        // The keypair stored here is used to sign auth challenges.
+        cluster_info: Arc<ClusterInfo>,
+        // Channel that server-sent heartbeats are piped through.
+        heartbeat_tx: Sender<HeartbeatEvent>,
+        // Channel that non-trusted streamed packets are piped through.
+        packet_tx: Sender<PacketBatch>,
+        // Channel that trusted streamed packets are piped through.
+        banking_packet_sender: BankingPacketSender,
+        exit: Arc<AtomicBool>,
+    ) -> Self {
+        let thread = Builder::new()
+            .name("relayer-stage".to_string())
+            .spawn(move || {
+                let rt = tokio::runtime::Builder::new_multi_thread()
+                    .enable_all()
+                    .build()
+                    .unwrap();
+
+                rt.block_on(Self::start(
+                    relayer_config,
+                    cluster_info,
+                    heartbeat_tx,
+                    packet_tx,
+                    banking_packet_sender,
+                    exit,
+                ));
+            })
+            .unwrap();
+
+        Self {
+            t_hdls: vec![thread],
+        }
+    }
+
+    pub fn join(self) -> thread::Result<()> {
+        for t in self.t_hdls {
+            t.join()?;
+        }
+        Ok(())
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    async fn start(
+        relayer_config: Arc<Mutex<RelayerConfig>>,
+        cluster_info: Arc<ClusterInfo>,
+        heartbeat_tx: Sender<HeartbeatEvent>,
+        packet_tx: Sender<PacketBatch>,
+        banking_packet_sender: BankingPacketSender,
+        exit: Arc<AtomicBool>,
+    ) {
+        const CONNECTION_TIMEOUT: Duration = Duration::from_secs(CONNECTION_TIMEOUT_S);
+        const CONNECTION_BACKOFF: Duration = Duration::from_secs(CONNECTION_BACKOFF_S);
+
+        let mut error_count: u64 = 0;
+
+        while !exit.load(Ordering::Relaxed) {
+            // Wait until a valid config is supplied (either initially or by admin rpc)
+            // Use if!/else here to avoid extra CONNECTION_BACKOFF wait on successful termination
+            if !Self::is_valid_relayer_config(&relayer_config.lock().unwrap()) {
+                sleep(CONNECTION_BACKOFF).await;
+            } else if let Err(e) = Self::connect_auth_and_stream(
+                &relayer_config,
+                &cluster_info,
+                &heartbeat_tx,
+                &packet_tx,
+                &banking_packet_sender,
+                &exit,
+                &CONNECTION_TIMEOUT,
+            )
+            .await
+            {
+                match e {
+                    // This error is frequent on hot spares, and the parsed string does not work
+                    // with datapoints (incorrect escaping).
+                    ProxyError::AuthenticationPermissionDenied => {
+                        warn!("relayer permission denied. not on leader schedule. ignore if hot-spare.")
+                    }
+                    e => {
+                        error_count += 1;
+                        datapoint_warn!(
+                            "relayer_stage-proxy_error",
+                            ("count", error_count, i64),
+                            ("error", e.to_string(), String),
+                        );
+                    }
+                }
+                sleep(CONNECTION_BACKOFF).await;
+            }
+        }
+    }
+
+    async fn connect_auth_and_stream(
+        relayer_config: &Arc<Mutex<RelayerConfig>>,
+        cluster_info: &Arc<ClusterInfo>,
+        heartbeat_tx: &Sender<HeartbeatEvent>,
+        packet_tx: &Sender<PacketBatch>,
+        banking_packet_sender: &BankingPacketSender,
+        exit: &Arc<AtomicBool>,
+        connection_timeout: &Duration,
+    ) -> crate::proxy::Result<()> {
+        // Get a copy of configs here in case they have changed at runtime
+        let keypair = cluster_info.keypair().clone();
+        let local_config = relayer_config.lock().unwrap().clone();
+
+        let mut backend_endpoint = Endpoint::from_shared(local_config.relayer_url.clone())
+            .map_err(|_| {
+                ProxyError::RelayerConnectionError(format!(
+                    "invalid relayer url value: {}",
+                    local_config.relayer_url
+                ))
+            })?
+            .tcp_keepalive(Some(Duration::from_secs(60)));
+        if local_config.relayer_url.starts_with("https") {
+            backend_endpoint = backend_endpoint
+                .tls_config(tonic::transport::ClientTlsConfig::new())
+                .map_err(|_| {
+                    ProxyError::RelayerConnectionError(
+                        "failed to set tls_config for relayer service".to_string(),
+                    )
+                })?;
+        }
+
+        debug!("connecting to auth: {}", local_config.relayer_url);
+        let auth_channel = timeout(*connection_timeout, backend_endpoint.connect())
+            .await
+            .map_err(|_| ProxyError::AuthenticationConnectionTimeout)?
+            .map_err(|e| ProxyError::AuthenticationConnectionError(e.to_string()))?;
+
+        let mut auth_client = AuthServiceClient::new(auth_channel);
+
+        debug!("generating authentication token");
+        let (access_token, refresh_token) = timeout(
+            *connection_timeout,
+            generate_auth_tokens(&mut auth_client, &keypair),
+        )
+        .await
+        .map_err(|_| ProxyError::AuthenticationTimeout)??;
+
+        datapoint_info!(
+            "relayer_stage-tokens_generated",
+            ("url", local_config.relayer_url, String),
+            ("count", 1, i64),
+        );
+
+        debug!("connecting to relayer: {}", local_config.relayer_url);
+        let relayer_channel = timeout(*connection_timeout, backend_endpoint.connect())
+            .await
+            .map_err(|_| ProxyError::RelayerConnectionTimeout)?
+            .map_err(|e| ProxyError::RelayerConnectionError(e.to_string()))?;
+
+        let access_token = Arc::new(Mutex::new(access_token));
+        let relayer_client = RelayerClient::with_interceptor(
+            relayer_channel,
+            AuthInterceptor::new(access_token.clone()),
+        );
+
+        Self::start_consuming_relayer_packets(
+            relayer_client,
+            heartbeat_tx,
+            packet_tx,
+            banking_packet_sender,
+            &local_config,
+            relayer_config,
+            exit,
+            auth_client,
+            access_token,
+            refresh_token,
+            keypair,
+            cluster_info,
+            connection_timeout,
+        )
+        .await
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    async fn start_consuming_relayer_packets(
+        mut client: RelayerClient<InterceptedService<Channel, AuthInterceptor>>,
+        heartbeat_tx: &Sender<HeartbeatEvent>,
+        packet_tx: &Sender<PacketBatch>,
+        banking_packet_sender: &BankingPacketSender,
+        local_config: &RelayerConfig, // local copy of config with current connections
+        global_config: &Arc<Mutex<RelayerConfig>>, // guarded reference for detecting run-time updates
+        exit: &Arc<AtomicBool>,
+        auth_client: AuthServiceClient<Channel>,
+        access_token: Arc<Mutex<Token>>,
+        refresh_token: Token,
+        keypair: Arc<Keypair>,
+        cluster_info: &Arc<ClusterInfo>,
+        connection_timeout: &Duration,
+    ) -> crate::proxy::Result<()> {
+        let heartbeat_event: HeartbeatEvent = {
+            let tpu_config = timeout(
+                *connection_timeout,
+                client.get_tpu_configs(relayer::GetTpuConfigsRequest {}),
+            )
+            .await
+            .map_err(|_| ProxyError::MethodTimeout("relayer_get_tpu_configs".to_string()))?
+            .map_err(|e| ProxyError::MethodError(e.to_string()))?
+            .into_inner();
+
+            let tpu_addr = tpu_config
+                .tpu
+                .ok_or_else(|| ProxyError::MissingTpuSocket("tpu".to_string()))?;
+            let tpu_forward_addr = tpu_config
+                .tpu_forward
+                .ok_or_else(|| ProxyError::MissingTpuSocket("tpu_fwd".to_string()))?;
+
+            let tpu_ip = IpAddr::from(tpu_addr.ip.parse::<Ipv4Addr>()?);
+            let tpu_forward_ip = IpAddr::from(tpu_forward_addr.ip.parse::<Ipv4Addr>()?);
+
+            let tpu_socket = SocketAddr::new(tpu_ip, tpu_addr.port as u16);
+            let tpu_forward_socket = SocketAddr::new(tpu_forward_ip, tpu_forward_addr.port as u16);
+            (tpu_socket, tpu_forward_socket)
+        };
+
+        let packet_stream = timeout(
+            *connection_timeout,
+            client.subscribe_packets(relayer::SubscribePacketsRequest {}),
+        )
+        .await
+        .map_err(|_| ProxyError::MethodTimeout("relayer_subscribe_packets".to_string()))?
+        .map_err(|e| ProxyError::MethodError(e.to_string()))?
+        .into_inner();
+
+        Self::consume_packet_stream(
+            heartbeat_event,
+            heartbeat_tx,
+            packet_stream,
+            packet_tx,
+            local_config,
+            global_config,
+            banking_packet_sender,
+            exit,
+            auth_client,
+            access_token,
+            refresh_token,
+            keypair,
+            cluster_info,
+            connection_timeout,
+        )
+        .await
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    async fn consume_packet_stream(
+        heartbeat_event: HeartbeatEvent,
+        heartbeat_tx: &Sender<HeartbeatEvent>,
+        mut packet_stream: Streaming<relayer::SubscribePacketsResponse>,
+        packet_tx: &Sender<PacketBatch>,
+        local_config: &RelayerConfig, // local copy of config with current connections
+        global_config: &Arc<Mutex<RelayerConfig>>, // guarded reference for detecting run-time updates
+        banking_packet_sender: &BankingPacketSender,
+        exit: &Arc<AtomicBool>,
+        mut auth_client: AuthServiceClient<Channel>,
+        access_token: Arc<Mutex<Token>>,
+        mut refresh_token: Token,
+        keypair: Arc<Keypair>,
+        cluster_info: &Arc<ClusterInfo>,
+        connection_timeout: &Duration,
+    ) -> crate::proxy::Result<()> {
+        const METRICS_TICK: Duration = Duration::from_secs(1);
+        let refresh_within_s: u64 = METRICS_TICK.as_secs().saturating_mul(3).saturating_div(2);
+
+        let mut relayer_stats = RelayerStageStats::default();
+        let mut metrics_and_auth_tick = interval(METRICS_TICK);
+
+        let mut num_full_refreshes: u64 = 1;
+        let mut num_refresh_access_token: u64 = 0;
+
+        let mut heartbeat_check_interval = interval(local_config.expected_heartbeat_interval);
+        let mut last_heartbeat_ts = Instant::now();
+
+        info!("connected to packet stream");
+
+        while !exit.load(Ordering::Relaxed) {
+            tokio::select! {
+                maybe_msg = packet_stream.message() => {
+                    let resp = maybe_msg?.ok_or(ProxyError::GrpcStreamDisconnected)?;
+                    Self::handle_relayer_packets(resp, heartbeat_event, heartbeat_tx, &mut last_heartbeat_ts, packet_tx, local_config.trust_packets, banking_packet_sender, &mut relayer_stats)?;
+                }
+                _ = heartbeat_check_interval.tick() => {
+                    if last_heartbeat_ts.elapsed() > local_config.oldest_allowed_heartbeat {
+                        return Err(ProxyError::HeartbeatExpired);
+                    }
+                }
+                _ = metrics_and_auth_tick.tick() => {
+                    relayer_stats.report();
+                    relayer_stats = RelayerStageStats::default();
+
+                    if cluster_info.id() != keypair.pubkey() {
+                        return Err(ProxyError::AuthenticationConnectionError("validator identity changed".to_string()));
+                    }
+
+                    if *global_config.lock().unwrap() != *local_config {
+                        return Err(ProxyError::AuthenticationConnectionError("relayer config changed".to_string()));
+                    }
+
+                    let (maybe_new_access, maybe_new_refresh) = maybe_refresh_auth_tokens(&mut auth_client,
+                        &access_token,
+                        &refresh_token,
+                        cluster_info,
+                        connection_timeout,
+                        refresh_within_s,
+                    ).await?;
+
+                    if let Some(new_token) = maybe_new_access {
+                        num_refresh_access_token += 1;
+                        datapoint_info!(
+                            "relayer_stage-refresh_access_token",
+                            ("url", &local_config.relayer_url, String),
+                            ("count", num_refresh_access_token, i64),
+                        );
+                        *access_token.lock().unwrap() = new_token;
+                    }
+                    if let Some(new_token) = maybe_new_refresh {
+                        num_full_refreshes += 1;
+                        datapoint_info!(
+                            "relayer_stage-tokens_generated",
+                            ("url", &local_config.relayer_url, String),
+                            ("count", num_full_refreshes, i64),
+                        );
+                        refresh_token = new_token;
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+
+    fn handle_relayer_packets(
+        subscribe_packets_resp: relayer::SubscribePacketsResponse,
+        heartbeat_event: HeartbeatEvent,
+        heartbeat_tx: &Sender<HeartbeatEvent>,
+        last_heartbeat_ts: &mut Instant,
+        packet_tx: &Sender<PacketBatch>,
+        trust_packets: bool,
+        banking_packet_sender: &BankingPacketSender,
+        relayer_stats: &mut RelayerStageStats,
+    ) -> crate::proxy::Result<()> {
+        match subscribe_packets_resp.msg {
+            None => {
+                saturating_add_assign!(relayer_stats.num_empty_messages, 1);
+            }
+            Some(relayer::subscribe_packets_response::Msg::Batch(proto_batch)) => {
+                if proto_batch.packets.is_empty() {
+                    saturating_add_assign!(relayer_stats.num_empty_messages, 1);
+                    return Ok(());
+                }
+
+                let packet_batch = PacketBatch::new(
+                    proto_batch
+                        .packets
+                        .into_iter()
+                        .map(proto_packet_to_packet)
+                        .collect(),
+                );
+
+                saturating_add_assign!(relayer_stats.num_packets, packet_batch.len() as u64);
+
+                if trust_packets {
+                    banking_packet_sender
+                        .send(Arc::new((vec![packet_batch], None)))
+                        .map_err(|_| ProxyError::PacketForwardError)?;
+                } else {
+                    packet_tx
+                        .send(packet_batch)
+                        .map_err(|_| ProxyError::PacketForwardError)?;
+                }
+            }
+            Some(relayer::subscribe_packets_response::Msg::Heartbeat(_)) => {
+                saturating_add_assign!(relayer_stats.num_heartbeats, 1);
+
+                *last_heartbeat_ts = Instant::now();
+                heartbeat_tx
+                    .send(heartbeat_event)
+                    .map_err(|_| ProxyError::HeartbeatChannelError)?;
+            }
+        }
+        Ok(())
+    }
+
+    pub fn is_valid_relayer_config(config: &RelayerConfig) -> bool {
+        if config.relayer_url.is_empty() {
+            warn!("can't connect to relayer. missing relayer_url.");
+            return false;
+        }
+        if config.oldest_allowed_heartbeat.is_zero() {
+            error!("can't connect to relayer. oldest allowed heartbeat must be greater than 0.");
+            return false;
+        }
+        if config.expected_heartbeat_interval.is_zero() {
+            error!("can't connect to relayer. expected heartbeat interval must be greater than 0.");
+            return false;
+        }
+        if let Err(e) = Endpoint::from_str(&config.relayer_url) {
+            error!(
+                "can't connect to relayer. error creating relayer endpoint - {}",
+                e.to_string()
+            );
+            return false;
+        }
+        true
+    }
+}
diff --git a/core/src/qos_service.rs b/core/src/qos_service.rs
index a27974a2b9..356aa90196 100644
--- a/core/src/qos_service.rs
+++ b/core/src/qos_service.rs
@@ -10,6 +10,7 @@ use {
     solana_runtime::{
         bank::Bank,
         cost_model::{CostModel, TransactionCost},
+        cost_tracker::CostTracker,
     },
     solana_sdk::{
         clock::Slot,
@@ -91,6 +92,7 @@ impl QosService {
     pub fn select_and_accumulate_transaction_costs(
         &self,
         bank: &Bank,
+        cost_tracker: &mut CostTracker, // caller should pass in &mut bank.write_cost_tracker().unwrap()
         transactions: &[SanitizedTransaction],
         pre_results: impl Iterator<Item = transaction::Result<()>>,
     ) -> (Vec<transaction::Result<TransactionCost>>, usize) {
@@ -99,7 +101,8 @@ impl QosService {
         let (transactions_qos_cost_results, num_included) = self.select_transactions_per_cost(
             transactions.iter(),
             transaction_costs.into_iter(),
-            bank,
+            bank.slot(),
+            cost_tracker,
         );
         self.accumulate_estimated_transaction_costs(&Self::accumulate_batched_transaction_costs(
             transactions_qos_cost_results.iter(),
@@ -145,10 +148,10 @@ impl QosService {
         &self,
         transactions: impl Iterator<Item = &'a SanitizedTransaction>,
         transactions_costs: impl Iterator<Item = transaction::Result<TransactionCost>>,
-        bank: &Bank,
+        slot: Slot,
+        cost_tracker: &mut CostTracker,
     ) -> (Vec<transaction::Result<TransactionCost>>, usize) {
         let mut cost_tracking_time = Measure::start("cost_tracking_time");
-        let mut cost_tracker = bank.write_cost_tracker().unwrap();
         let mut num_included = 0;
         let select_results = transactions.zip(transactions_costs)
             .map(|(tx, cost)| {
@@ -156,13 +159,13 @@ impl QosService {
                     Ok(cost) => {
                         match cost_tracker.try_add(&cost) {
                             Ok(current_block_cost) => {
-                                debug!("slot {:?}, transaction {:?}, cost {:?}, fit into current block, current block cost {}", bank.slot(), tx, cost, current_block_cost);
+                                debug!("slot {:?}, transaction {:?}, cost {:?}, fit into current block, current block cost {}", slot, tx, cost, current_block_cost);
                                 self.metrics.stats.selected_txs_count.fetch_add(1, Ordering::Relaxed);
                                 num_included += 1;
                                 Ok(cost)
                             },
                             Err(e) => {
-                                debug!("slot {:?}, transaction {:?}, cost {:?}, not fit into current block, '{:?}'", bank.slot(), tx, cost, e);
+                                debug!("slot {:?}, transaction {:?}, cost {:?}, not fit into current block, '{:?}'", slot, tx, cost, e);
                                 Err(TransactionError::from(e))
                             }
                         }
@@ -751,8 +754,12 @@ mod tests {
         bank.write_cost_tracker()
             .unwrap()
             .set_limits(cost_limit, cost_limit, cost_limit);
-        let (results, num_selected) =
-            qos_service.select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank);
+        let (results, num_selected) = qos_service.select_transactions_per_cost(
+            txs.iter(),
+            txs_costs.into_iter(),
+            bank.slot(),
+            &mut bank.write_cost_tracker().unwrap(),
+        );
         assert_eq!(num_selected, 2);
 
         // verify that first transfer tx and first vote are allowed
@@ -793,8 +800,12 @@ mod tests {
                 .iter()
                 .map(|cost| cost.as_ref().unwrap().sum())
                 .sum();
-            let (qos_cost_results, _num_included) =
-                qos_service.select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank);
+            let (qos_cost_results, _num_included) = qos_service.select_transactions_per_cost(
+                txs.iter(),
+                txs_costs.into_iter(),
+                bank.slot(),
+                &mut bank.write_cost_tracker().unwrap(),
+            );
             assert_eq!(
                 total_txs_cost,
                 bank.read_cost_tracker().unwrap().block_cost()
@@ -861,8 +872,12 @@ mod tests {
                 .iter()
                 .map(|cost| cost.as_ref().unwrap().sum())
                 .sum();
-            let (qos_cost_results, _num_included) =
-                qos_service.select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank);
+            let (qos_cost_results, _num_included) = qos_service.select_transactions_per_cost(
+                txs.iter(),
+                txs_costs.into_iter(),
+                bank.slot(),
+                &mut bank.write_cost_tracker().unwrap(),
+            );
             assert_eq!(
                 total_txs_cost,
                 bank.read_cost_tracker().unwrap().block_cost()
@@ -915,8 +930,12 @@ mod tests {
                 .iter()
                 .map(|cost| cost.as_ref().unwrap().sum())
                 .sum();
-            let (qos_cost_results, _num_included) =
-                qos_service.select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank);
+            let (qos_cost_results, _num_included) = qos_service.select_transactions_per_cost(
+                txs.iter(),
+                txs_costs.into_iter(),
+                bank.slot(),
+                &mut bank.write_cost_tracker().unwrap(),
+            );
             assert_eq!(
                 total_txs_cost,
                 bank.read_cost_tracker().unwrap().block_cost()
diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs
index 62813a6748..cb7e4dfe60 100644
--- a/core/src/replay_stage.rs
+++ b/core/src/replay_stage.rs
@@ -2360,7 +2360,9 @@ impl ReplayStage {
 
         // If we are a non voting validator or have an incorrect setup preventing us from
         // generating vote txs, no need to refresh
-        let Some(last_vote_tx_blockhash) = tower.last_vote_tx_blockhash() else { return };
+        let Some(last_vote_tx_blockhash) = tower.last_vote_tx_blockhash() else {
+            return;
+        };
 
         if my_latest_landed_vote >= last_voted_slot
             || heaviest_bank_on_same_fork
diff --git a/core/src/retransmit_stage.rs b/core/src/retransmit_stage.rs
index 96eeffff8c..7887143f63 100644
--- a/core/src/retransmit_stage.rs
+++ b/core/src/retransmit_stage.rs
@@ -29,7 +29,7 @@ use {
     std::{
         collections::HashMap,
         iter::repeat,
-        net::UdpSocket,
+        net::{SocketAddr, UdpSocket},
         ops::AddAssign,
         sync::{
             atomic::{AtomicU64, AtomicUsize, Ordering},
@@ -178,6 +178,7 @@ fn retransmit(
     shred_deduper: &mut ShredDeduper<2>,
     max_slots: &MaxSlots,
     rpc_subscriptions: Option<&RpcSubscriptions>,
+    shred_receiver_address: &Arc<RwLock<Option<SocketAddr>>>,
 ) -> Result<(), RecvTimeoutError> {
     const RECV_TIMEOUT: Duration = Duration::from_secs(1);
     let mut shreds = shreds_receiver.recv_timeout(RECV_TIMEOUT)?;
@@ -260,6 +261,7 @@ fn retransmit(
                     socket_addr_space,
                     &sockets[index % sockets.len()],
                     stats,
+                    &shred_receiver_address.read().unwrap(),
                 )
                 .map_err(|err| {
                     stats.record_error(&err);
@@ -284,6 +286,7 @@ fn retransmit(
                         socket_addr_space,
                         &sockets[index % sockets.len()],
                         stats,
+                        &shred_receiver_address.read().unwrap(),
                     )
                     .map_err(|err| {
                         stats.record_error(&err);
@@ -312,15 +315,20 @@ fn retransmit_shred(
     socket_addr_space: &SocketAddrSpace,
     socket: &UdpSocket,
     stats: &RetransmitStats,
+    shred_receiver_addr: &Option<SocketAddr>,
 ) -> Result<(/*root_distance:*/ usize, /*num_nodes:*/ usize), Error> {
     let mut compute_turbine_peers = Measure::start("turbine_start");
     let data_plane_fanout = cluster_nodes::get_data_plane_fanout(key.slot(), root_bank);
     let (root_distance, addrs) =
         cluster_nodes.get_retransmit_addrs(slot_leader, key, root_bank, data_plane_fanout)?;
-    let addrs: Vec<_> = addrs
+    let mut addrs: Vec<_> = addrs
         .into_iter()
         .filter(|addr| ContactInfo::is_valid_address(addr, socket_addr_space))
         .collect();
+    if let Some(addr) = shred_receiver_addr {
+        addrs.push(*addr);
+    }
+
     compute_turbine_peers.stop();
     stats
         .compute_turbine_peers_total
@@ -366,6 +374,7 @@ pub fn retransmitter(
     shreds_receiver: Receiver<Vec</*shred:*/ Vec<u8>>>,
     max_slots: Arc<MaxSlots>,
     rpc_subscriptions: Option<Arc<RpcSubscriptions>>,
+    shred_receiver_addr: Arc<RwLock<Option<SocketAddr>>>,
 ) -> JoinHandle<()> {
     let cluster_nodes_cache = ClusterNodesCache::<RetransmitStage>::new(
         CLUSTER_NODES_CACHE_NUM_EPOCH_CAP,
@@ -396,6 +405,7 @@ pub fn retransmitter(
                 &mut shred_deduper,
                 &max_slots,
                 rpc_subscriptions.as_deref(),
+                &shred_receiver_addr,
             ) {
                 Ok(()) => (),
                 Err(RecvTimeoutError::Timeout) => (),
@@ -418,6 +428,7 @@ impl RetransmitStage {
         retransmit_receiver: Receiver<Vec</*shred:*/ Vec<u8>>>,
         max_slots: Arc<MaxSlots>,
         rpc_subscriptions: Option<Arc<RpcSubscriptions>>,
+        shred_receiver_addr: Arc<RwLock<Option<SocketAddr>>>,
     ) -> Self {
         let retransmit_thread_handle = retransmitter(
             retransmit_sockets,
@@ -427,6 +438,7 @@ impl RetransmitStage {
             retransmit_receiver,
             max_slots,
             rpc_subscriptions,
+            shred_receiver_addr,
         );
 
         Self {
diff --git a/core/src/snapshot_packager_service.rs b/core/src/snapshot_packager_service.rs
index 5974d076ed..0094098588 100644
--- a/core/src/snapshot_packager_service.rs
+++ b/core/src/snapshot_packager_service.rs
@@ -51,13 +51,13 @@ impl SnapshotPackagerService {
             .spawn(move || {
                 info!("SnapshotPackagerService has started");
                 renice_this_thread(snapshot_config.packager_thread_niceness_adj).unwrap();
-                let mut snapshot_gossip_manager = enable_gossip_push.then(||
+                let mut snapshot_gossip_manager = enable_gossip_push.then(|| {
                     SnapshotGossipManager::new(
                         cluster_info,
                         max_full_snapshot_hashes,
                         starting_snapshot_hashes,
                     )
-                );
+                });
 
                 loop {
                     if exit.load(Ordering::Relaxed) {
@@ -68,7 +68,11 @@ impl SnapshotPackagerService {
                         snapshot_package,
                         num_outstanding_snapshot_packages,
                         num_re_enqueued_snapshot_packages,
-                    )) = Self::get_next_snapshot_package(&snapshot_package_sender, &snapshot_package_receiver) else {
+                    )) = Self::get_next_snapshot_package(
+                        &snapshot_package_sender,
+                        &snapshot_package_receiver,
+                    )
+                    else {
                         std::thread::sleep(Self::LOOP_LIMITER);
                         continue;
                     };
@@ -102,7 +106,8 @@ impl SnapshotPackagerService {
                         measure_us!(snapshot_utils::purge_bank_snapshots_older_than_slot(
                             &snapshot_config.bank_snapshots_dir,
                             snapshot_package.slot(),
-                        )).1
+                        ))
+                        .1
                     });
 
                     datapoint_info!(
@@ -119,7 +124,11 @@ impl SnapshotPackagerService {
                         ),
                         ("enqueued_time_us", enqueued_time.as_micros(), i64),
                         ("handling_time_us", handling_time_us, i64),
-                        ("purge_old_snapshots_time_us", purge_bank_snapshots_time_us, i64),
+                        (
+                            "purge_old_snapshots_time_us",
+                            purge_bank_snapshots_time_us,
+                            i64
+                        ),
                     );
                 }
                 info!("SnapshotPackagerService has stopped");
diff --git a/core/src/tip_manager.rs b/core/src/tip_manager.rs
new file mode 100644
index 0000000000..724abbbe02
--- /dev/null
+++ b/core/src/tip_manager.rs
@@ -0,0 +1,583 @@
+use {
+    crate::proxy::block_engine_stage::BlockBuilderFeeInfo,
+    anchor_lang::{
+        solana_program::hash::Hash, AccountDeserialize, InstructionData, ToAccountMetas,
+    },
+    jito_tip_distribution::sdk::{
+        derive_config_account_address, derive_tip_distribution_account_address,
+        instruction::{
+            initialize_ix, initialize_tip_distribution_account_ix, InitializeAccounts,
+            InitializeArgs, InitializeTipDistributionAccountAccounts,
+            InitializeTipDistributionAccountArgs,
+        },
+    },
+    jito_tip_payment::{
+        Config, InitBumps, TipPaymentAccount, CONFIG_ACCOUNT_SEED, TIP_ACCOUNT_SEED_0,
+        TIP_ACCOUNT_SEED_1, TIP_ACCOUNT_SEED_2, TIP_ACCOUNT_SEED_3, TIP_ACCOUNT_SEED_4,
+        TIP_ACCOUNT_SEED_5, TIP_ACCOUNT_SEED_6, TIP_ACCOUNT_SEED_7,
+    },
+    log::warn,
+    solana_bundle::TipError,
+    solana_runtime::bank::Bank,
+    solana_sdk::{
+        account::ReadableAccount,
+        bundle::{derive_bundle_id_from_sanitized_transactions, SanitizedBundle},
+        instruction::Instruction,
+        pubkey::Pubkey,
+        signature::Keypair,
+        signer::Signer,
+        stake_history::Epoch,
+        system_program,
+        transaction::{SanitizedTransaction, Transaction},
+    },
+    std::{collections::HashSet, sync::Arc},
+};
+
+pub type Result<T> = std::result::Result<T, TipError>;
+
+#[derive(Debug, Clone)]
+struct TipPaymentProgramInfo {
+    program_id: Pubkey,
+
+    config_pda_bump: (Pubkey, u8),
+    tip_pda_0: (Pubkey, u8),
+    tip_pda_1: (Pubkey, u8),
+    tip_pda_2: (Pubkey, u8),
+    tip_pda_3: (Pubkey, u8),
+    tip_pda_4: (Pubkey, u8),
+    tip_pda_5: (Pubkey, u8),
+    tip_pda_6: (Pubkey, u8),
+    tip_pda_7: (Pubkey, u8),
+}
+
+/// Contains metadata regarding the tip-distribution account.
+/// The PDAs contained in this struct are presumed to be owned by the program.
+#[derive(Debug, Clone)]
+struct TipDistributionProgramInfo {
+    /// The tip-distribution program_id.
+    program_id: Pubkey,
+
+    /// Singleton [Config] PDA and bump tuple.
+    config_pda_and_bump: (Pubkey, u8),
+}
+
+/// This config is used on each invocation to the `initialize_tip_distribution_account` instruction.
+#[derive(Debug, Clone)]
+pub struct TipDistributionAccountConfig {
+    /// The account with authority to upload merkle-roots to this validator's [TipDistributionAccount].
+    pub merkle_root_upload_authority: Pubkey,
+
+    /// This validator's vote account.
+    pub vote_account: Pubkey,
+
+    /// This validator's commission rate BPS for tips in the [TipDistributionAccount].
+    pub commission_bps: u16,
+}
+
+impl Default for TipDistributionAccountConfig {
+    fn default() -> Self {
+        Self {
+            merkle_root_upload_authority: Pubkey::new_unique(),
+            vote_account: Pubkey::new_unique(),
+            commission_bps: 0,
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct TipManager {
+    tip_payment_program_info: TipPaymentProgramInfo,
+    tip_distribution_program_info: TipDistributionProgramInfo,
+    tip_distribution_account_config: TipDistributionAccountConfig,
+}
+
+#[derive(Clone)]
+pub struct TipManagerConfig {
+    pub tip_payment_program_id: Pubkey,
+    pub tip_distribution_program_id: Pubkey,
+    pub tip_distribution_account_config: TipDistributionAccountConfig,
+}
+
+impl Default for TipManagerConfig {
+    fn default() -> Self {
+        TipManagerConfig {
+            tip_payment_program_id: Pubkey::new_unique(),
+            tip_distribution_program_id: Pubkey::new_unique(),
+            tip_distribution_account_config: TipDistributionAccountConfig::default(),
+        }
+    }
+}
+
+impl TipManager {
+    pub fn new(config: TipManagerConfig) -> TipManager {
+        let TipManagerConfig {
+            tip_payment_program_id,
+            tip_distribution_program_id,
+            tip_distribution_account_config,
+        } = config;
+
+        let config_pda_bump =
+            Pubkey::find_program_address(&[CONFIG_ACCOUNT_SEED], &tip_payment_program_id);
+
+        let tip_pda_0 =
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_0], &tip_payment_program_id);
+        let tip_pda_1 =
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_1], &tip_payment_program_id);
+        let tip_pda_2 =
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_2], &tip_payment_program_id);
+        let tip_pda_3 =
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_3], &tip_payment_program_id);
+        let tip_pda_4 =
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_4], &tip_payment_program_id);
+        let tip_pda_5 =
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_5], &tip_payment_program_id);
+        let tip_pda_6 =
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_6], &tip_payment_program_id);
+        let tip_pda_7 =
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_7], &tip_payment_program_id);
+
+        let config_pda_and_bump = derive_config_account_address(&tip_distribution_program_id);
+
+        TipManager {
+            tip_payment_program_info: TipPaymentProgramInfo {
+                program_id: tip_payment_program_id,
+                config_pda_bump,
+                tip_pda_0,
+                tip_pda_1,
+                tip_pda_2,
+                tip_pda_3,
+                tip_pda_4,
+                tip_pda_5,
+                tip_pda_6,
+                tip_pda_7,
+            },
+            tip_distribution_program_info: TipDistributionProgramInfo {
+                program_id: tip_distribution_program_id,
+                config_pda_and_bump,
+            },
+            tip_distribution_account_config,
+        }
+    }
+
+    pub fn tip_payment_program_id(&self) -> Pubkey {
+        self.tip_payment_program_info.program_id
+    }
+
+    pub fn tip_distribution_program_id(&self) -> Pubkey {
+        self.tip_distribution_program_info.program_id
+    }
+
+    /// Returns the [Config] account owned by the tip-payment program.
+    pub fn tip_payment_config_pubkey(&self) -> Pubkey {
+        self.tip_payment_program_info.config_pda_bump.0
+    }
+
+    /// Returns the [Config] account owned by the tip-distribution program.
+    pub fn tip_distribution_config_pubkey(&self) -> Pubkey {
+        self.tip_distribution_program_info.config_pda_and_bump.0
+    }
+
+    /// Given a bank, returns the current `tip_receiver` configured with the tip-payment program.
+    pub fn get_configured_tip_receiver(&self, bank: &Bank) -> Result<Pubkey> {
+        Ok(self.get_tip_payment_config_account(bank)?.tip_receiver)
+    }
+
+    pub fn get_tip_accounts(&self) -> HashSet<Pubkey> {
+        HashSet::from([
+            self.tip_payment_program_info.tip_pda_0.0,
+            self.tip_payment_program_info.tip_pda_1.0,
+            self.tip_payment_program_info.tip_pda_2.0,
+            self.tip_payment_program_info.tip_pda_3.0,
+            self.tip_payment_program_info.tip_pda_4.0,
+            self.tip_payment_program_info.tip_pda_5.0,
+            self.tip_payment_program_info.tip_pda_6.0,
+            self.tip_payment_program_info.tip_pda_7.0,
+        ])
+    }
+
+    pub fn get_tip_payment_config_account(&self, bank: &Bank) -> Result<Config> {
+        let config_data = bank
+            .get_account(&self.tip_payment_program_info.config_pda_bump.0)
+            .ok_or(TipError::AccountMissing(
+                self.tip_payment_program_info.config_pda_bump.0,
+            ))?;
+
+        Ok(Config::try_deserialize(&mut config_data.data())?)
+    }
+
+    /// Only called once during contract creation.
+    pub fn initialize_tip_payment_program_tx(
+        &self,
+        recent_blockhash: Hash,
+        keypair: &Keypair,
+    ) -> SanitizedTransaction {
+        let init_ix = Instruction {
+            program_id: self.tip_payment_program_info.program_id,
+            data: jito_tip_payment::instruction::Initialize {
+                _bumps: InitBumps {
+                    config: self.tip_payment_program_info.config_pda_bump.1,
+                    tip_payment_account_0: self.tip_payment_program_info.tip_pda_0.1,
+                    tip_payment_account_1: self.tip_payment_program_info.tip_pda_1.1,
+                    tip_payment_account_2: self.tip_payment_program_info.tip_pda_2.1,
+                    tip_payment_account_3: self.tip_payment_program_info.tip_pda_3.1,
+                    tip_payment_account_4: self.tip_payment_program_info.tip_pda_4.1,
+                    tip_payment_account_5: self.tip_payment_program_info.tip_pda_5.1,
+                    tip_payment_account_6: self.tip_payment_program_info.tip_pda_6.1,
+                    tip_payment_account_7: self.tip_payment_program_info.tip_pda_7.1,
+                },
+            }
+            .data(),
+            accounts: jito_tip_payment::accounts::Initialize {
+                config: self.tip_payment_program_info.config_pda_bump.0,
+                tip_payment_account_0: self.tip_payment_program_info.tip_pda_0.0,
+                tip_payment_account_1: self.tip_payment_program_info.tip_pda_1.0,
+                tip_payment_account_2: self.tip_payment_program_info.tip_pda_2.0,
+                tip_payment_account_3: self.tip_payment_program_info.tip_pda_3.0,
+                tip_payment_account_4: self.tip_payment_program_info.tip_pda_4.0,
+                tip_payment_account_5: self.tip_payment_program_info.tip_pda_5.0,
+                tip_payment_account_6: self.tip_payment_program_info.tip_pda_6.0,
+                tip_payment_account_7: self.tip_payment_program_info.tip_pda_7.0,
+                system_program: system_program::id(),
+                payer: keypair.pubkey(),
+            }
+            .to_account_metas(None),
+        };
+        SanitizedTransaction::try_from_legacy_transaction(Transaction::new_signed_with_payer(
+            &[init_ix],
+            Some(&keypair.pubkey()),
+            &[keypair],
+            recent_blockhash,
+        ))
+        .unwrap()
+    }
+
+    /// Returns this validator's [TipDistributionAccount] PDA derived from the provided epoch.
+    pub fn get_my_tip_distribution_pda(&self, epoch: Epoch) -> Pubkey {
+        derive_tip_distribution_account_address(
+            &self.tip_distribution_program_info.program_id,
+            &self.tip_distribution_account_config.vote_account,
+            epoch,
+        )
+        .0
+    }
+
+    /// Returns whether or not the tip-payment program should be initialized.
+    pub fn should_initialize_tip_payment_program(&self, bank: &Bank) -> bool {
+        match bank.get_account(&self.tip_payment_config_pubkey()) {
+            None => true,
+            Some(account) => account.owner() != &self.tip_payment_program_info.program_id,
+        }
+    }
+
+    /// Returns whether or not the tip-distribution program's [Config] PDA should be initialized.
+    pub fn should_initialize_tip_distribution_config(&self, bank: &Bank) -> bool {
+        match bank.get_account(&self.tip_distribution_config_pubkey()) {
+            None => true,
+            Some(account) => account.owner() != &self.tip_distribution_program_info.program_id,
+        }
+    }
+
+    /// Returns whether or not the current [TipDistributionAccount] PDA should be initialized for this epoch.
+    pub fn should_init_tip_distribution_account(&self, bank: &Bank) -> bool {
+        let pda = derive_tip_distribution_account_address(
+            &self.tip_distribution_program_info.program_id,
+            &self.tip_distribution_account_config.vote_account,
+            bank.epoch(),
+        )
+        .0;
+        match bank.get_account(&pda) {
+            None => true,
+            // Since anyone can derive the PDA and send it lamports we must also check the owner is the program.
+            Some(account) => account.owner() != &self.tip_distribution_program_info.program_id,
+        }
+    }
+
+    /// Creates an [Initialize] transaction object.
+    pub fn initialize_tip_distribution_config_tx(
+        &self,
+        recent_blockhash: Hash,
+        kp: &Keypair,
+    ) -> SanitizedTransaction {
+        let ix = initialize_ix(
+            self.tip_distribution_program_info.program_id,
+            InitializeArgs {
+                authority: kp.pubkey(),
+                expired_funds_account: kp.pubkey(),
+                num_epochs_valid: 10,
+                max_validator_commission_bps: 10_000,
+                bump: self.tip_distribution_program_info.config_pda_and_bump.1,
+            },
+            InitializeAccounts {
+                config: self.tip_distribution_program_info.config_pda_and_bump.0,
+                system_program: system_program::id(),
+                initializer: kp.pubkey(),
+            },
+        );
+
+        SanitizedTransaction::try_from_legacy_transaction(Transaction::new_signed_with_payer(
+            &[ix],
+            Some(&kp.pubkey()),
+            &[kp],
+            recent_blockhash,
+        ))
+        .unwrap()
+    }
+
+    /// Creates an [InitializeTipDistributionAccount] transaction object using the provided Epoch.
+    pub fn initialize_tip_distribution_account_tx(
+        &self,
+        recent_blockhash: Hash,
+        epoch: Epoch,
+        keypair: &Keypair,
+    ) -> SanitizedTransaction {
+        let (tip_distribution_account, bump) = derive_tip_distribution_account_address(
+            &self.tip_distribution_program_info.program_id,
+            &self.tip_distribution_account_config.vote_account,
+            epoch,
+        );
+
+        let ix = initialize_tip_distribution_account_ix(
+            self.tip_distribution_program_info.program_id,
+            InitializeTipDistributionAccountArgs {
+                merkle_root_upload_authority: self
+                    .tip_distribution_account_config
+                    .merkle_root_upload_authority,
+                validator_commission_bps: self.tip_distribution_account_config.commission_bps,
+                bump,
+            },
+            InitializeTipDistributionAccountAccounts {
+                config: self.tip_distribution_program_info.config_pda_and_bump.0,
+                tip_distribution_account,
+                system_program: system_program::id(),
+                signer: keypair.pubkey(),
+                validator_vote_account: self.tip_distribution_account_config.vote_account,
+            },
+        );
+
+        SanitizedTransaction::try_from_legacy_transaction(Transaction::new_signed_with_payer(
+            &[ix],
+            Some(&keypair.pubkey()),
+            &[keypair],
+            recent_blockhash,
+        ))
+        .unwrap()
+    }
+
+    /// Builds a transaction that changes the current tip receiver to new_tip_receiver.
+    /// The on-chain program will transfer tips sitting in the tip accounts to the tip receiver
+    /// before changing ownership.
+    pub fn change_tip_receiver_and_block_builder_tx(
+        &self,
+        new_tip_receiver: &Pubkey,
+        bank: &Bank,
+        keypair: &Keypair,
+        block_builder: &Pubkey,
+        block_builder_commission: u64,
+    ) -> Result<SanitizedTransaction> {
+        let config = self.get_tip_payment_config_account(bank)?;
+        Ok(self.build_change_tip_receiver_and_block_builder_tx(
+            &config.tip_receiver,
+            new_tip_receiver,
+            bank,
+            keypair,
+            &config.block_builder,
+            block_builder,
+            block_builder_commission,
+        ))
+    }
+
+    pub fn build_change_tip_receiver_and_block_builder_tx(
+        &self,
+        old_tip_receiver: &Pubkey,
+        new_tip_receiver: &Pubkey,
+        bank: &Bank,
+        keypair: &Keypair,
+        old_block_builder: &Pubkey,
+        block_builder: &Pubkey,
+        block_builder_commission: u64,
+    ) -> SanitizedTransaction {
+        let change_tip_ix = Instruction {
+            program_id: self.tip_payment_program_info.program_id,
+            data: jito_tip_payment::instruction::ChangeTipReceiver {}.data(),
+            accounts: jito_tip_payment::accounts::ChangeTipReceiver {
+                config: self.tip_payment_program_info.config_pda_bump.0,
+                old_tip_receiver: *old_tip_receiver,
+                new_tip_receiver: *new_tip_receiver,
+                block_builder: *old_block_builder,
+                tip_payment_account_0: self.tip_payment_program_info.tip_pda_0.0,
+                tip_payment_account_1: self.tip_payment_program_info.tip_pda_1.0,
+                tip_payment_account_2: self.tip_payment_program_info.tip_pda_2.0,
+                tip_payment_account_3: self.tip_payment_program_info.tip_pda_3.0,
+                tip_payment_account_4: self.tip_payment_program_info.tip_pda_4.0,
+                tip_payment_account_5: self.tip_payment_program_info.tip_pda_5.0,
+                tip_payment_account_6: self.tip_payment_program_info.tip_pda_6.0,
+                tip_payment_account_7: self.tip_payment_program_info.tip_pda_7.0,
+                signer: keypair.pubkey(),
+            }
+            .to_account_metas(None),
+        };
+        let change_block_builder_ix = Instruction {
+            program_id: self.tip_payment_program_info.program_id,
+            data: jito_tip_payment::instruction::ChangeBlockBuilder {
+                block_builder_commission,
+            }
+            .data(),
+            accounts: jito_tip_payment::accounts::ChangeBlockBuilder {
+                config: self.tip_payment_program_info.config_pda_bump.0,
+                tip_receiver: *new_tip_receiver, // tip receiver will have just changed in previous ix
+                old_block_builder: *old_block_builder,
+                new_block_builder: *block_builder,
+                tip_payment_account_0: self.tip_payment_program_info.tip_pda_0.0,
+                tip_payment_account_1: self.tip_payment_program_info.tip_pda_1.0,
+                tip_payment_account_2: self.tip_payment_program_info.tip_pda_2.0,
+                tip_payment_account_3: self.tip_payment_program_info.tip_pda_3.0,
+                tip_payment_account_4: self.tip_payment_program_info.tip_pda_4.0,
+                tip_payment_account_5: self.tip_payment_program_info.tip_pda_5.0,
+                tip_payment_account_6: self.tip_payment_program_info.tip_pda_6.0,
+                tip_payment_account_7: self.tip_payment_program_info.tip_pda_7.0,
+                signer: keypair.pubkey(),
+            }
+            .to_account_metas(None),
+        };
+        SanitizedTransaction::try_from_legacy_transaction(Transaction::new_signed_with_payer(
+            &[change_tip_ix, change_block_builder_ix],
+            Some(&keypair.pubkey()),
+            &[keypair],
+            bank.last_blockhash(),
+        ))
+        .unwrap()
+    }
+
+    /// Returns the balance of all the MEV tip accounts
+    pub fn get_tip_account_balances(&self, bank: &Arc<Bank>) -> Vec<(Pubkey, u64)> {
+        let accounts = self.get_tip_accounts();
+        accounts
+            .into_iter()
+            .map(|account| {
+                let balance = bank.get_balance(&account);
+                (account, balance)
+            })
+            .collect()
+    }
+
+    /// Returns the balance of all the MEV tip accounts above the rent-exempt amount.
+    /// NOTE: the on-chain program has rent_exempt = force
+    pub fn get_tip_account_balances_above_rent_exempt(
+        &self,
+        bank: &Arc<Bank>,
+    ) -> Vec<(Pubkey, u64)> {
+        let accounts = self.get_tip_accounts();
+        accounts
+            .into_iter()
+            .map(|account| {
+                let account_data = bank.get_account(&account).unwrap_or_default();
+                let balance = bank.get_balance(&account);
+                let rent_exempt =
+                    bank.get_minimum_balance_for_rent_exemption(account_data.data().len());
+                // NOTE: don't unwrap here in case bug in on-chain program, don't want all validators to crash
+                // if program gets stuck in bad state
+                (account, balance.checked_sub(rent_exempt).unwrap_or_else(|| {
+                    warn!("balance is below rent exempt amount. balance: {} rent_exempt: {} acc size: {}", balance, rent_exempt, TipPaymentAccount::SIZE);
+                    0
+                }))
+            })
+            .collect()
+    }
+
+    /// Return a bundle that is capable of calling the initialize instructions on the two tip payment programs
+    /// This is mainly helpful for local development and shouldn't run on testnet and mainnet, assuming the
+    /// correct TipManager configuration is set.
+    pub fn get_initialize_tip_programs_bundle(
+        &self,
+        bank: &Bank,
+        keypair: &Keypair,
+    ) -> Option<SanitizedBundle> {
+        let maybe_init_tip_payment_config_tx = if self.should_initialize_tip_payment_program(bank) {
+            debug!("should_initialize_tip_payment_program=true");
+            Some(self.initialize_tip_payment_program_tx(bank.last_blockhash(), keypair))
+        } else {
+            None
+        };
+
+        let maybe_init_tip_distro_config_tx =
+            if self.should_initialize_tip_distribution_config(bank) {
+                debug!("should_initialize_tip_distribution_config=true");
+                Some(self.initialize_tip_distribution_config_tx(bank.last_blockhash(), keypair))
+            } else {
+                None
+            };
+
+        let transactions = [
+            maybe_init_tip_payment_config_tx,
+            maybe_init_tip_distro_config_tx,
+        ]
+        .into_iter()
+        .flatten()
+        .collect::<Vec<SanitizedTransaction>>();
+
+        if transactions.is_empty() {
+            None
+        } else {
+            let bundle_id = derive_bundle_id_from_sanitized_transactions(&transactions);
+            Some(SanitizedBundle {
+                transactions,
+                bundle_id,
+            })
+        }
+    }
+
+    pub fn get_tip_programs_crank_bundle(
+        &self,
+        bank: &Bank,
+        keypair: &Keypair,
+        block_builder_fee_info: &BlockBuilderFeeInfo,
+    ) -> Result<Option<SanitizedBundle>> {
+        let maybe_init_tip_distro_account_tx = if self.should_init_tip_distribution_account(bank) {
+            debug!("should_init_tip_distribution_account=true");
+            Some(self.initialize_tip_distribution_account_tx(
+                bank.last_blockhash(),
+                bank.epoch(),
+                keypair,
+            ))
+        } else {
+            None
+        };
+
+        let configured_tip_receiver = self.get_configured_tip_receiver(bank)?;
+        let my_tip_receiver = self.get_my_tip_distribution_pda(bank.epoch());
+        let maybe_change_tip_receiver_tx = if configured_tip_receiver != my_tip_receiver {
+            debug!("change_tip_receiver=true");
+            Some(self.change_tip_receiver_and_block_builder_tx(
+                &my_tip_receiver,
+                bank,
+                keypair,
+                &block_builder_fee_info.block_builder,
+                block_builder_fee_info.block_builder_commission,
+            )?)
+        } else {
+            None
+        };
+        debug!(
+            "maybe_change_tip_receiver_tx: {:?}",
+            maybe_change_tip_receiver_tx
+        );
+
+        let transactions = [
+            maybe_init_tip_distro_account_tx,
+            maybe_change_tip_receiver_tx,
+        ]
+        .into_iter()
+        .flatten()
+        .collect::<Vec<SanitizedTransaction>>();
+
+        if transactions.is_empty() {
+            Ok(None)
+        } else {
+            let bundle_id = derive_bundle_id_from_sanitized_transactions(&transactions);
+            Ok(Some(SanitizedBundle {
+                transactions,
+                bundle_id,
+            }))
+        }
+    }
+}
diff --git a/core/src/tpu.rs b/core/src/tpu.rs
index f129b2fbed..8545a0d3be 100644
--- a/core/src/tpu.rs
+++ b/core/src/tpu.rs
@@ -7,14 +7,21 @@ use {
         banking_stage::BankingStage,
         banking_trace::{BankingTracer, TracerThread},
         broadcast_stage::{BroadcastStage, BroadcastStageType, RetransmitSlotsReceiver},
+        bundle_stage::{bundle_account_locker::BundleAccountLocker, BundleStage},
         cluster_info_vote_listener::{
             ClusterInfoVoteListener, GossipDuplicateConfirmedSlotsSender,
             GossipVerifiedVoteHashSender, VerifiedVoteSender, VoteTracker,
         },
         fetch_stage::FetchStage,
+        proxy::{
+            block_engine_stage::{BlockBuilderFeeInfo, BlockEngineConfig, BlockEngineStage},
+            fetch_stage_manager::FetchStageManager,
+            relayer_stage::{RelayerConfig, RelayerStage},
+        },
         sigverify::TransactionSigVerifier,
         sigverify_stage::SigVerifyStage,
         staked_nodes_updater_service::StakedNodesUpdaterService,
+        tip_manager::{TipManager, TipManagerConfig},
         tpu_entry_notifier::TpuEntryNotifier,
         validator::GeneratorConfig,
     },
@@ -35,16 +42,19 @@ use {
         prioritization_fee_cache::PrioritizationFeeCache,
         vote_sender_types::{ReplayVoteReceiver, ReplayVoteSender},
     },
-    solana_sdk::{pubkey::Pubkey, signature::Keypair},
+    solana_sdk::{
+        pubkey::Pubkey,
+        signature::{Keypair, Signer},
+    },
     solana_streamer::{
         nonblocking::quic::DEFAULT_WAIT_FOR_CHUNK_TIMEOUT,
         quic::{spawn_server, MAX_STAKED_CONNECTIONS, MAX_UNSTAKED_CONNECTIONS},
         streamer::StakedNodes,
     },
     std::{
-        collections::HashMap,
-        net::UdpSocket,
-        sync::{atomic::AtomicBool, Arc, RwLock},
+        collections::{HashMap, HashSet},
+        net::{SocketAddr, UdpSocket},
+        sync::{atomic::AtomicBool, Arc, Mutex, RwLock},
         thread,
         time::Duration,
     },
@@ -66,6 +76,9 @@ pub struct Tpu {
     fetch_stage: FetchStage,
     sigverify_stage: SigVerifyStage,
     vote_sigverify_stage: SigVerifyStage,
+    relayer_stage: RelayerStage,
+    block_engine_stage: BlockEngineStage,
+    fetch_stage_manager: FetchStageManager,
     banking_stage: BankingStage,
     cluster_info_vote_listener: ClusterInfoVoteListener,
     broadcast_stage: BroadcastStage,
@@ -74,6 +87,7 @@ pub struct Tpu {
     tpu_entry_notifier: Option<TpuEntryNotifier>,
     staked_nodes_updater_service: StakedNodesUpdaterService,
     tracer_thread_hdl: TracerThread,
+    bundle_stage: BundleStage,
 }
 
 impl Tpu {
@@ -104,12 +118,17 @@ impl Tpu {
         keypair: &Keypair,
         log_messages_bytes_limit: Option<usize>,
         staked_nodes: &Arc<RwLock<StakedNodes>>,
+        block_engine_config: Arc<Mutex<BlockEngineConfig>>,
+        relayer_config: Arc<Mutex<RelayerConfig>>,
+        tip_manager_config: TipManagerConfig,
+        shred_receiver_address: Arc<RwLock<Option<SocketAddr>>>,
         shared_staked_nodes_overrides: Arc<RwLock<HashMap<Pubkey, u64>>>,
         banking_tracer: Arc<BankingTracer>,
         tracer_thread_hdl: TracerThread,
         tpu_enable_udp: bool,
         prioritization_fee_cache: &Arc<PrioritizationFeeCache>,
         _generator_config: Option<GeneratorConfig>, /* vestigial code for replay invalidator */
+        preallocated_bundle_cost: u64,
     ) -> Self {
         let TpuSockets {
             transactions: transactions_sockets,
@@ -120,7 +139,10 @@ impl Tpu {
             transactions_forwards_quic: transactions_forwards_quic_sockets,
         } = sockets;
 
-        let (packet_sender, packet_receiver) = unbounded();
+        // Packets from fetch stage and quic server are intercepted and sent through fetch_stage_manager
+        // If relayer is connected, packets are dropped. If not, packets are forwarded on to packet_sender
+        let (packet_intercept_sender, packet_intercept_receiver) = unbounded();
+
         let (vote_packet_sender, vote_packet_receiver) = unbounded();
         let (forwarded_packet_sender, forwarded_packet_receiver) = unbounded();
         let fetch_stage = FetchStage::new_with_sender(
@@ -128,7 +150,7 @@ impl Tpu {
             tpu_forwards_sockets,
             tpu_vote_sockets,
             exit,
-            &packet_sender,
+            &packet_intercept_sender,
             &vote_packet_sender,
             &forwarded_packet_sender,
             forwarded_packet_receiver,
@@ -156,7 +178,7 @@ impl Tpu {
                 .tpu(Protocol::QUIC)
                 .expect("Operator must spin up node with valid (QUIC) TPU address")
                 .ip(),
-            packet_sender,
+            packet_intercept_sender,
             exit.clone(),
             MAX_QUIC_CONNECTIONS_PER_PEER,
             staked_nodes.clone(),
@@ -187,8 +209,10 @@ impl Tpu {
         )
         .unwrap();
 
+        let (packet_sender, packet_receiver) = unbounded();
+
         let sigverify_stage = {
-            let verifier = TransactionSigVerifier::new(non_vote_sender);
+            let verifier = TransactionSigVerifier::new(non_vote_sender.clone());
             SigVerifyStage::new(packet_receiver, verifier, "tpu-verifier")
         };
 
@@ -201,6 +225,41 @@ impl Tpu {
 
         let (gossip_vote_sender, gossip_vote_receiver) =
             banking_tracer.create_channel_gossip_vote();
+
+        let block_builder_fee_info = Arc::new(Mutex::new(BlockBuilderFeeInfo {
+            block_builder: cluster_info.keypair().pubkey(),
+            block_builder_commission: 0,
+        }));
+
+        let (bundle_sender, bundle_receiver) = unbounded();
+        let block_engine_stage = BlockEngineStage::new(
+            block_engine_config,
+            bundle_sender,
+            cluster_info.clone(),
+            packet_sender.clone(),
+            non_vote_sender.clone(),
+            exit.clone(),
+            &block_builder_fee_info,
+        );
+
+        let (heartbeat_tx, heartbeat_rx) = unbounded();
+        let fetch_stage_manager = FetchStageManager::new(
+            cluster_info.clone(),
+            heartbeat_rx,
+            packet_intercept_receiver,
+            packet_sender.clone(),
+            exit.clone(),
+        );
+
+        let relayer_stage = RelayerStage::new(
+            relayer_config,
+            cluster_info.clone(),
+            heartbeat_tx,
+            packet_sender,
+            non_vote_sender,
+            exit.clone(),
+        );
+
         let cluster_info_vote_listener = ClusterInfoVoteListener::new(
             exit.clone(),
             cluster_info.clone(),
@@ -217,16 +276,43 @@ impl Tpu {
             cluster_confirmed_slot_sender,
         );
 
+        let tip_manager = TipManager::new(tip_manager_config);
+
+        let bundle_account_locker = BundleAccountLocker::default();
+
+        // tip accounts can't be used in BankingStage to avoid someone from stealing tips mid-slot.
+        // it also helps reduce surface area for potential account contention
+        let mut blacklisted_accounts = HashSet::new();
+        blacklisted_accounts.insert(tip_manager.tip_payment_config_pubkey());
+        blacklisted_accounts.extend(tip_manager.get_tip_accounts());
         let banking_stage = BankingStage::new(
             cluster_info,
             poh_recorder,
             non_vote_receiver,
             tpu_vote_receiver,
             gossip_vote_receiver,
+            transaction_status_sender.clone(),
+            replay_vote_sender.clone(),
+            log_messages_bytes_limit,
+            connection_cache.clone(),
+            bank_forks.clone(),
+            prioritization_fee_cache,
+            blacklisted_accounts,
+            bundle_account_locker.clone(),
+        );
+
+        let bundle_stage = BundleStage::new(
+            cluster_info,
+            poh_recorder,
+            bundle_receiver,
             transaction_status_sender,
             replay_vote_sender,
             log_messages_bytes_limit,
-            connection_cache.clone(),
+            exit.clone(),
+            tip_manager,
+            bundle_account_locker,
+            &block_builder_fee_info,
+            preallocated_bundle_cost,
             bank_forks.clone(),
             prioritization_fee_cache,
         );
@@ -254,12 +340,16 @@ impl Tpu {
             blockstore.clone(),
             bank_forks,
             shred_version,
+            shred_receiver_address,
         );
 
         Self {
             fetch_stage,
             sigverify_stage,
             vote_sigverify_stage,
+            block_engine_stage,
+            relayer_stage,
+            fetch_stage_manager,
             banking_stage,
             cluster_info_vote_listener,
             broadcast_stage,
@@ -268,6 +358,7 @@ impl Tpu {
             tpu_entry_notifier,
             staked_nodes_updater_service,
             tracer_thread_hdl,
+            bundle_stage,
         }
     }
 
@@ -281,6 +372,10 @@ impl Tpu {
             self.staked_nodes_updater_service.join(),
             self.tpu_quic_t.join(),
             self.tpu_forwards_quic_t.join(),
+            self.bundle_stage.join(),
+            self.relayer_stage.join(),
+            self.block_engine_stage.join(),
+            self.fetch_stage_manager.join(),
         ];
         let broadcast_result = self.broadcast_stage.join();
         for result in results {
diff --git a/core/src/tpu_entry_notifier.rs b/core/src/tpu_entry_notifier.rs
index 730a3b14fa..bc20696f5b 100644
--- a/core/src/tpu_entry_notifier.rs
+++ b/core/src/tpu_entry_notifier.rs
@@ -58,40 +58,54 @@ impl TpuEntryNotifier {
         current_slot: &mut u64,
         current_index: &mut usize,
     ) -> Result<(), RecvTimeoutError> {
-        let (bank, (entry, tick_height)) = entry_receiver.recv_timeout(Duration::from_secs(1))?;
+        let WorkingBankEntry {
+            bank,
+            entries_ticks,
+        } = entry_receiver.recv_timeout(Duration::from_secs(1))?;
         let slot = bank.slot();
-        let index = if slot != *current_slot {
-            *current_index = 0;
-            *current_slot = slot;
-            0
-        } else {
-            *current_index += 1;
-            *current_index
-        };
 
-        let entry_summary = EntrySummary {
-            num_hashes: entry.num_hashes,
-            hash: entry.hash,
-            num_transactions: entry.transactions.len() as u64,
-        };
-        if let Err(err) = entry_notification_sender.send(EntryNotification {
-            slot,
-            index,
-            entry: entry_summary,
-        }) {
-            warn!(
+        let mut indices_sent = vec![];
+
+        entries_ticks.iter().for_each(|(entry, _)| {
+            let index = if slot != *current_slot {
+                *current_index = 0;
+                *current_slot = slot;
+                0
+            } else {
+                *current_index += 1;
+                *current_index
+            };
+
+            let entry_summary = EntrySummary {
+                num_hashes: entry.num_hashes,
+                hash: entry.hash,
+                num_transactions: entry.transactions.len() as u64,
+            };
+            if let Err(err) = entry_notification_sender.send(EntryNotification {
+                slot,
+                index,
+                entry: entry_summary,
+            }) {
+                warn!(
                 "Failed to send slot {slot:?} entry {index:?} from Tpu to EntryNotifierService, error {err:?}",
             );
-        }
+            }
+
+            indices_sent.push(index);
+        });
 
-        if let Err(err) = broadcast_entry_sender.send((bank, (entry, tick_height))) {
+        if let Err(err) = broadcast_entry_sender.send(WorkingBankEntry {
+            bank,
+            entries_ticks,
+        }) {
             warn!(
-                "Failed to send slot {slot:?} entry {index:?} from Tpu to BroadcastStage, error {err:?}",
+                "Failed to send slot {slot:?} entries {indices_sent:?} from Tpu to BroadcastStage, error {err:?}",
             );
             // If the BroadcastStage channel is closed, the validator has halted. Try to exit
             // gracefully.
             exit.store(true, Ordering::Relaxed);
         }
+
         Ok(())
     }
 
diff --git a/core/src/tvu.rs b/core/src/tvu.rs
index a5a7fba451..d83f2bd573 100644
--- a/core/src/tvu.rs
+++ b/core/src/tvu.rs
@@ -52,7 +52,7 @@ use {
     solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Keypair},
     std::{
         collections::HashSet,
-        net::UdpSocket,
+        net::{SocketAddr, UdpSocket},
         sync::{atomic::AtomicBool, Arc, RwLock},
         thread::{self, JoinHandle},
     },
@@ -138,6 +138,7 @@ impl Tvu {
         connection_cache: &Arc<ConnectionCache>,
         prioritization_fee_cache: &Arc<PrioritizationFeeCache>,
         banking_tracer: Arc<BankingTracer>,
+        shred_receiver_addr: Arc<RwLock<Option<SocketAddr>>>,
     ) -> Result<Self, String> {
         let TvuSockets {
             repair: repair_socket,
@@ -185,6 +186,7 @@ impl Tvu {
             retransmit_receiver,
             max_slots.clone(),
             Some(rpc_subscriptions.clone()),
+            shred_receiver_addr,
         );
 
         let cluster_slots = Arc::new(ClusterSlots::default());
@@ -483,6 +485,7 @@ pub mod tests {
             &Arc::new(ConnectionCache::new("connection_cache_test")),
             &ignored_prioritization_fee_cache,
             BankingTracer::new_disabled(),
+            Arc::new(RwLock::new(None)),
         )
         .expect("assume success");
         exit.store(true, Ordering::Relaxed);
diff --git a/core/src/unprocessed_transaction_storage.rs b/core/src/unprocessed_transaction_storage.rs
index 53d6005a55..d0428dd8c6 100644
--- a/core/src/unprocessed_transaction_storage.rs
+++ b/core/src/unprocessed_transaction_storage.rs
@@ -1,7 +1,9 @@
 use {
     crate::{
         banking_stage::{BankingStageStats, FilterForwardingResults, ForwardOption},
+        bundle_stage::bundle_stage_leader_metrics::BundleStageLeaderMetrics,
         forward_packet_batches_by_accounts::ForwardPacketBatchesByAccounts,
+        immutable_deserialized_bundle::ImmutableDeserializedBundle,
         immutable_deserialized_packet::ImmutableDeserializedPacket,
         latest_unprocessed_votes::{
             LatestUnprocessedVotes, LatestValidatorVotePacket, VoteBatchInsertionMetrics,
@@ -16,15 +18,22 @@ use {
     },
     itertools::Itertools,
     min_max_heap::MinMaxHeap,
+    solana_bundle::BundleExecutionError,
     solana_measure::measure,
-    solana_runtime::bank::Bank,
+    solana_runtime::{bank::Bank, transaction_error_metrics::TransactionErrorMetrics},
     solana_sdk::{
-        clock::FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET, feature_set::FeatureSet, hash::Hash,
-        saturating_add_assign, transaction::SanitizedTransaction,
+        bundle::SanitizedBundle,
+        clock::{Slot, FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET},
+        feature_set::FeatureSet,
+        hash::Hash,
+        pubkey::Pubkey,
+        saturating_add_assign,
+        transaction::SanitizedTransaction,
     },
     std::{
-        collections::HashMap,
+        collections::{HashMap, HashSet, VecDeque},
         sync::{atomic::Ordering, Arc},
+        time::Instant,
     },
 };
 
@@ -39,6 +48,7 @@ const MAX_NUM_VOTES_RECEIVE: usize = 10_000;
 pub enum UnprocessedTransactionStorage {
     VoteStorage(VoteStorage),
     LocalTransactionStorage(ThreadLocalUnprocessedPackets),
+    BundleStorage(BundleStorage),
 }
 
 #[derive(Debug)]
@@ -57,10 +67,11 @@ pub struct VoteStorage {
 pub enum ThreadType {
     Voting(VoteSource),
     Transactions,
+    Bundles,
 }
 
 #[derive(Debug)]
-pub(crate) enum InsertPacketBatchSummary {
+pub enum InsertPacketBatchSummary {
     VoteBatchInsertionMetrics(VoteBatchInsertionMetrics),
     PacketBatchInsertionMetrics(PacketBatchInsertionMetrics),
 }
@@ -143,6 +154,7 @@ fn consume_scan_should_process_packet(
     banking_stage_stats: &BankingStageStats,
     packet: &ImmutableDeserializedPacket,
     payload: &mut ConsumeScannerPayload,
+    blacklisted_accounts: &HashSet<Pubkey>,
 ) -> ProcessingDecision {
     // If end of the slot, return should process (quick loop after reached end of slot)
     if payload.reached_end_of_slot {
@@ -177,6 +189,10 @@ fn consume_scan_should_process_packet(
             bank.get_transaction_account_lock_limit(),
         )
         .is_err()
+            || message
+                .account_keys()
+                .iter()
+                .any(|key| blacklisted_accounts.contains(key))
         {
             payload
                 .message_hash_to_transaction
@@ -245,10 +261,24 @@ impl UnprocessedTransactionStorage {
         })
     }
 
+    pub fn new_bundle_storage(
+        unprocessed_bundle_storage: VecDeque<ImmutableDeserializedBundle>,
+        cost_model_failed_bundles: VecDeque<ImmutableDeserializedBundle>,
+    ) -> Self {
+        Self::BundleStorage(BundleStorage {
+            last_update_slot: Slot::default(),
+            unprocessed_bundle_storage,
+            cost_model_buffered_bundle_storage: cost_model_failed_bundles,
+        })
+    }
+
     pub fn is_empty(&self) -> bool {
         match self {
             Self::VoteStorage(vote_storage) => vote_storage.is_empty(),
             Self::LocalTransactionStorage(transaction_storage) => transaction_storage.is_empty(),
+            UnprocessedTransactionStorage::BundleStorage(bundle_storage) => {
+                bundle_storage.is_empty()
+            }
         }
     }
 
@@ -256,6 +286,10 @@ impl UnprocessedTransactionStorage {
         match self {
             Self::VoteStorage(vote_storage) => vote_storage.len(),
             Self::LocalTransactionStorage(transaction_storage) => transaction_storage.len(),
+            UnprocessedTransactionStorage::BundleStorage(bundle_storage) => {
+                bundle_storage.unprocessed_bundles_len()
+                    + bundle_storage.cost_model_buffered_bundles_len()
+            }
         }
     }
 
@@ -266,6 +300,9 @@ impl UnprocessedTransactionStorage {
             Self::LocalTransactionStorage(transaction_storage) => {
                 transaction_storage.max_receive_size()
             }
+            UnprocessedTransactionStorage::BundleStorage(bundle_storage) => {
+                bundle_storage.max_receive_size()
+            }
         }
     }
 
@@ -292,6 +329,9 @@ impl UnprocessedTransactionStorage {
             Self::LocalTransactionStorage(transaction_storage) => {
                 transaction_storage.forward_option()
             }
+            UnprocessedTransactionStorage::BundleStorage(bundle_storage) => {
+                bundle_storage.forward_option()
+            }
         }
     }
 
@@ -299,6 +339,16 @@ impl UnprocessedTransactionStorage {
         match self {
             Self::LocalTransactionStorage(transaction_storage) => transaction_storage.clear(), // Since we set everything as forwarded this is the same
             Self::VoteStorage(vote_storage) => vote_storage.clear_forwarded_packets(),
+            UnprocessedTransactionStorage::BundleStorage(bundle_storage) => {
+                let _ = bundle_storage.reset();
+            }
+        }
+    }
+
+    pub fn bundle_storage(&mut self) -> Option<&mut BundleStorage> {
+        match self {
+            UnprocessedTransactionStorage::BundleStorage(bundle_stoge) => Some(bundle_stoge),
+            _ => None,
         }
     }
 
@@ -313,6 +363,11 @@ impl UnprocessedTransactionStorage {
             Self::LocalTransactionStorage(transaction_storage) => InsertPacketBatchSummary::from(
                 transaction_storage.insert_batch(deserialized_packets),
             ),
+            UnprocessedTransactionStorage::BundleStorage(_) => {
+                panic!(
+                    "bundles must be inserted using UnprocessedTransactionStorage::insert_bundle"
+                )
+            }
         }
     }
 
@@ -332,6 +387,9 @@ impl UnprocessedTransactionStorage {
                     bank,
                     forward_packet_batches_by_accounts,
                 ),
+            UnprocessedTransactionStorage::BundleStorage(_) => {
+                panic!("bundles are not forwarded between leaders")
+            }
         }
     }
 
@@ -345,6 +403,7 @@ impl UnprocessedTransactionStorage {
         banking_stage_stats: &BankingStageStats,
         slot_metrics_tracker: &mut LeaderSlotMetricsTracker,
         processing_function: F,
+        blacklisted_accounts: &HashSet<Pubkey>,
     ) -> bool
     where
         F: FnMut(
@@ -359,15 +418,62 @@ impl UnprocessedTransactionStorage {
                     banking_stage_stats,
                     slot_metrics_tracker,
                     processing_function,
+                    blacklisted_accounts,
                 ),
             Self::VoteStorage(vote_storage) => vote_storage.process_packets(
                 bank,
                 banking_stage_stats,
                 slot_metrics_tracker,
                 processing_function,
+                blacklisted_accounts,
+            ),
+            UnprocessedTransactionStorage::BundleStorage(_) => panic!(
+                "UnprocessedTransactionStorage::BundleStorage does not support processing packets"
             ),
         }
     }
+
+    #[must_use]
+    pub fn process_bundles<F>(
+        &mut self,
+        bank: Arc<Bank>,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+        blacklisted_accounts: &HashSet<Pubkey>,
+        processing_function: F,
+    ) -> bool
+    where
+        F: FnMut(
+            &[(ImmutableDeserializedBundle, SanitizedBundle)],
+            &mut BundleStageLeaderMetrics,
+        ) -> Vec<Result<(), BundleExecutionError>>,
+    {
+        match self {
+            UnprocessedTransactionStorage::BundleStorage(bundle_storage) => bundle_storage
+                .process_bundles(
+                    bank,
+                    bundle_stage_leader_metrics,
+                    blacklisted_accounts,
+                    processing_function,
+                ),
+            _ => panic!("class does not support processing bundles"),
+        }
+    }
+
+    /// Inserts bundles into storage. Only supported for UnprocessedTransactionStorage::BundleStorage
+    pub(crate) fn insert_bundles(
+        &mut self,
+        deserialized_bundles: Vec<ImmutableDeserializedBundle>,
+    ) -> InsertPacketBundlesSummary {
+        match self {
+            UnprocessedTransactionStorage::BundleStorage(bundle_storage) => {
+                bundle_storage.insert_unprocessed_bundles(deserialized_bundles, true)
+            }
+            UnprocessedTransactionStorage::LocalTransactionStorage(_)
+            | UnprocessedTransactionStorage::VoteStorage(_) => {
+                panic!("UnprocessedTransactionStorage::insert_bundles only works for type UnprocessedTransactionStorage::BundleStorage");
+            }
+        }
+    }
 }
 
 impl VoteStorage {
@@ -436,6 +542,7 @@ impl VoteStorage {
         banking_stage_stats: &BankingStageStats,
         slot_metrics_tracker: &mut LeaderSlotMetricsTracker,
         mut processing_function: F,
+        blacklisted_accounts: &HashSet<Pubkey>,
     ) -> bool
     where
         F: FnMut(
@@ -449,7 +556,13 @@ impl VoteStorage {
 
         let should_process_packet =
             |packet: &Arc<ImmutableDeserializedPacket>, payload: &mut ConsumeScannerPayload| {
-                consume_scan_should_process_packet(&bank, banking_stage_stats, packet, payload)
+                consume_scan_should_process_packet(
+                    &bank,
+                    banking_stage_stats,
+                    packet,
+                    payload,
+                    blacklisted_accounts,
+                )
             };
 
         // Based on the stake distribution present in the supplied bank, drain the unprocessed votes
@@ -524,6 +637,7 @@ impl ThreadLocalUnprocessedPackets {
             ThreadType::Transactions => ForwardOption::ForwardTransaction,
             ThreadType::Voting(VoteSource::Tpu) => ForwardOption::ForwardTpuVote,
             ThreadType::Voting(VoteSource::Gossip) => ForwardOption::NotForward,
+            ThreadType::Bundles => panic!(), // TODO (LB)
         }
     }
 
@@ -852,6 +966,7 @@ impl ThreadLocalUnprocessedPackets {
         banking_stage_stats: &BankingStageStats,
         slot_metrics_tracker: &mut LeaderSlotMetricsTracker,
         mut processing_function: F,
+        blacklisted_accounts: &HashSet<Pubkey>,
     ) -> bool
     where
         F: FnMut(
@@ -866,7 +981,13 @@ impl ThreadLocalUnprocessedPackets {
 
         let should_process_packet =
             |packet: &Arc<ImmutableDeserializedPacket>, payload: &mut ConsumeScannerPayload| {
-                consume_scan_should_process_packet(bank, banking_stage_stats, packet, payload)
+                consume_scan_should_process_packet(
+                    bank,
+                    banking_stage_stats,
+                    packet,
+                    payload,
+                    blacklisted_accounts,
+                )
             };
         let mut scanner = create_consume_multi_iterator(
             &all_packets_to_process,
@@ -943,396 +1064,305 @@ impl ThreadLocalUnprocessedPackets {
     }
 }
 
-#[cfg(test)]
-mod tests {
-    use {
-        super::*,
-        solana_ledger::genesis_utils::{create_genesis_config, GenesisConfigInfo},
-        solana_perf::packet::{Packet, PacketFlags},
-        solana_sdk::{
-            hash::Hash,
-            signature::{Keypair, Signer},
-            system_transaction,
-            transaction::Transaction,
-        },
-        solana_vote_program::{
-            vote_state::VoteStateUpdate, vote_transaction::new_vote_state_update_transaction,
-        },
-        std::error::Error,
-    };
+pub struct InsertPacketBundlesSummary {
+    pub insert_packets_summary: InsertPacketBatchSummary,
+    pub num_bundles_inserted: usize,
+    pub num_packets_inserted: usize,
+    pub num_bundles_dropped: usize,
+}
 
-    #[test]
-    fn test_filter_processed_packets() {
-        let retryable_indexes = [0, 1, 2, 3];
-        let mut non_retryable_indexes = vec![];
-        let f = |start, end| {
-            non_retryable_indexes.push((start, end));
-        };
-        filter_processed_packets(retryable_indexes.iter(), f);
-        assert!(non_retryable_indexes.is_empty());
-
-        let retryable_indexes = [0, 1, 2, 3, 5];
-        let mut non_retryable_indexes = vec![];
-        let f = |start, end| {
-            non_retryable_indexes.push((start, end));
-        };
-        filter_processed_packets(retryable_indexes.iter(), f);
-        assert_eq!(non_retryable_indexes, vec![(4, 5)]);
-
-        let retryable_indexes = [1, 2, 3];
-        let mut non_retryable_indexes = vec![];
-        let f = |start, end| {
-            non_retryable_indexes.push((start, end));
-        };
-        filter_processed_packets(retryable_indexes.iter(), f);
-        assert_eq!(non_retryable_indexes, vec![(0, 1)]);
-
-        let retryable_indexes = [1, 2, 3, 5];
-        let mut non_retryable_indexes = vec![];
-        let f = |start, end| {
-            non_retryable_indexes.push((start, end));
-        };
-        filter_processed_packets(retryable_indexes.iter(), f);
-        assert_eq!(non_retryable_indexes, vec![(0, 1), (4, 5)]);
-
-        let retryable_indexes = [1, 2, 3, 5, 8];
-        let mut non_retryable_indexes = vec![];
-        let f = |start, end| {
-            non_retryable_indexes.push((start, end));
-        };
-        filter_processed_packets(retryable_indexes.iter(), f);
-        assert_eq!(non_retryable_indexes, vec![(0, 1), (4, 5), (6, 8)]);
-
-        let retryable_indexes = [1, 2, 3, 5, 8, 8];
-        let mut non_retryable_indexes = vec![];
-        let f = |start, end| {
-            non_retryable_indexes.push((start, end));
-        };
-        filter_processed_packets(retryable_indexes.iter(), f);
-        assert_eq!(non_retryable_indexes, vec![(0, 1), (4, 5), (6, 8)]);
-    }
-
-    #[test]
-    fn test_filter_and_forward_with_account_limits() {
-        solana_logger::setup();
-        let GenesisConfigInfo {
-            genesis_config,
-            mint_keypair,
-            ..
-        } = create_genesis_config(10);
-        let current_bank = Arc::new(Bank::new_for_tests(&genesis_config));
-
-        let simple_transactions: Vec<Transaction> = (0..256)
-            .map(|_id| {
-                // packets are deserialized upon receiving, failed packets will not be
-                // forwarded; Therefore we need to create real packets here.
-                let key1 = Keypair::new();
-                system_transaction::transfer(
-                    &mint_keypair,
-                    &key1.pubkey(),
-                    genesis_config.rent.minimum_balance(0),
-                    genesis_config.hash(),
-                )
-            })
-            .collect_vec();
+/// Bundle storage has two deques: one for unprocessed bundles and another for ones that exceeded
+/// the cost model and need to get retried next slot.
+#[derive(Debug)]
+pub struct BundleStorage {
+    last_update_slot: Slot,
+    unprocessed_bundle_storage: VecDeque<ImmutableDeserializedBundle>,
+    // Storage for bundles that exceeded the cost model for the slot they were last attempted
+    // execution on
+    cost_model_buffered_bundle_storage: VecDeque<ImmutableDeserializedBundle>,
+}
+
+impl BundleStorage {
+    fn is_empty(&self) -> bool {
+        self.unprocessed_bundle_storage.is_empty()
+    }
+
+    pub fn unprocessed_bundles_len(&self) -> usize {
+        self.unprocessed_bundle_storage.len()
+    }
 
-        let mut packets: Vec<DeserializedPacket> = simple_transactions
+    pub fn unprocessed_packets_len(&self) -> usize {
+        self.unprocessed_bundle_storage
             .iter()
-            .enumerate()
-            .map(|(packets_id, transaction)| {
-                let mut p = Packet::from_data(None, transaction).unwrap();
-                p.meta_mut().port = packets_id as u16;
-                p.meta_mut().set_tracer(true);
-                DeserializedPacket::new(p).unwrap()
-            })
-            .collect_vec();
+            .map(|b| b.len())
+            .sum::<usize>()
+    }
 
-        // all packets are forwarded
-        {
-            let buffered_packet_batches: UnprocessedPacketBatches =
-                UnprocessedPacketBatches::from_iter(packets.clone().into_iter(), packets.len());
-            let mut transaction_storage = UnprocessedTransactionStorage::new_transaction_storage(
-                buffered_packet_batches,
-                ThreadType::Transactions,
-            );
-            let mut forward_packet_batches_by_accounts =
-                ForwardPacketBatchesByAccounts::new_with_default_batch_limits();
+    pub(crate) fn cost_model_buffered_bundles_len(&self) -> usize {
+        self.cost_model_buffered_bundle_storage.len()
+    }
 
-            let FilterForwardingResults {
-                total_forwardable_packets,
-                total_tracer_packets_in_buffer,
-                total_forwardable_tracer_packets,
-                ..
-            } = transaction_storage.filter_forwardable_packets_and_add_batches(
-                current_bank.clone(),
-                &mut forward_packet_batches_by_accounts,
-            );
-            assert_eq!(total_forwardable_packets, 256);
-            assert_eq!(total_tracer_packets_in_buffer, 256);
-            assert_eq!(total_forwardable_tracer_packets, 256);
-
-            // packets in a batch are forwarded in arbitrary order; verify the ports match after
-            // sorting
-            let expected_ports: Vec<_> = (0..256).collect();
-            let mut forwarded_ports: Vec<_> = forward_packet_batches_by_accounts
-                .iter_batches()
-                .flat_map(|batch| batch.get_forwardable_packets().map(|p| p.meta().port))
-                .collect();
-            forwarded_ports.sort_unstable();
-            assert_eq!(expected_ports, forwarded_ports);
-        }
+    pub(crate) fn cost_model_buffered_packets_len(&self) -> usize {
+        self.cost_model_buffered_bundle_storage
+            .iter()
+            .map(|b| b.len())
+            .sum()
+    }
 
-        // some packets are forwarded
-        {
-            let num_already_forwarded = 16;
-            for packet in &mut packets[0..num_already_forwarded] {
-                packet.forwarded = true;
+    pub(crate) fn max_receive_size(&self) -> usize {
+        self.unprocessed_bundle_storage.capacity() - self.unprocessed_bundle_storage.len()
+    }
+
+    fn forward_option(&self) -> ForwardOption {
+        ForwardOption::NotForward
+    }
+
+    /// Returns the number of unprocessed bundles + cost model buffered cleared
+    pub fn reset(&mut self) -> (usize, usize) {
+        let num_unprocessed_bundles = self.unprocessed_bundle_storage.len();
+        let num_cost_model_buffered_bundles = self.cost_model_buffered_bundle_storage.len();
+        self.unprocessed_bundle_storage.clear();
+        self.cost_model_buffered_bundle_storage.clear();
+        (num_unprocessed_bundles, num_cost_model_buffered_bundles)
+    }
+
+    fn insert_bundles(
+        deque: &mut VecDeque<ImmutableDeserializedBundle>,
+        deserialized_bundles: Vec<ImmutableDeserializedBundle>,
+        push_back: bool,
+    ) -> InsertPacketBundlesSummary {
+        let mut num_bundles_inserted: usize = 0;
+        let mut num_packets_inserted: usize = 0;
+        let mut num_bundles_dropped: usize = 0;
+        let mut num_packets_dropped: usize = 0;
+
+        for bundle in deserialized_bundles {
+            if deque.capacity() == deque.len() {
+                saturating_add_assign!(num_bundles_dropped, 1);
+                saturating_add_assign!(num_packets_dropped, bundle.len());
+            } else {
+                saturating_add_assign!(num_bundles_inserted, 1);
+                saturating_add_assign!(num_packets_inserted, bundle.len());
+                if push_back {
+                    deque.push_back(bundle);
+                } else {
+                    deque.push_front(bundle)
+                }
             }
-            let buffered_packet_batches: UnprocessedPacketBatches =
-                UnprocessedPacketBatches::from_iter(packets.clone().into_iter(), packets.len());
-            let mut transaction_storage = UnprocessedTransactionStorage::new_transaction_storage(
-                buffered_packet_batches,
-                ThreadType::Transactions,
-            );
-            let mut forward_packet_batches_by_accounts =
-                ForwardPacketBatchesByAccounts::new_with_default_batch_limits();
-            let FilterForwardingResults {
-                total_forwardable_packets,
-                total_tracer_packets_in_buffer,
-                total_forwardable_tracer_packets,
-                ..
-            } = transaction_storage.filter_forwardable_packets_and_add_batches(
-                current_bank.clone(),
-                &mut forward_packet_batches_by_accounts,
-            );
-            assert_eq!(
-                total_forwardable_packets,
-                packets.len() - num_already_forwarded
-            );
-            assert_eq!(total_tracer_packets_in_buffer, packets.len());
-            assert_eq!(
-                total_forwardable_tracer_packets,
-                packets.len() - num_already_forwarded
-            );
         }
 
-        // some packets are invalid (already processed)
-        {
-            let num_already_processed = 16;
-            for tx in &simple_transactions[0..num_already_processed] {
-                assert_eq!(current_bank.process_transaction(tx), Ok(()));
+        InsertPacketBundlesSummary {
+            insert_packets_summary: PacketBatchInsertionMetrics {
+                num_dropped_packets: num_packets_dropped,
+                num_dropped_tracer_packets: 0,
             }
-            let buffered_packet_batches: UnprocessedPacketBatches =
-                UnprocessedPacketBatches::from_iter(packets.clone().into_iter(), packets.len());
-            let mut transaction_storage = UnprocessedTransactionStorage::new_transaction_storage(
-                buffered_packet_batches,
-                ThreadType::Transactions,
-            );
-            let mut forward_packet_batches_by_accounts =
-                ForwardPacketBatchesByAccounts::new_with_default_batch_limits();
-            let FilterForwardingResults {
-                total_forwardable_packets,
-                total_tracer_packets_in_buffer,
-                total_forwardable_tracer_packets,
-                ..
-            } = transaction_storage.filter_forwardable_packets_and_add_batches(
-                current_bank,
-                &mut forward_packet_batches_by_accounts,
-            );
-            assert_eq!(
-                total_forwardable_packets,
-                packets.len() - num_already_processed
-            );
-            assert_eq!(total_tracer_packets_in_buffer, packets.len());
-            assert_eq!(
-                total_forwardable_tracer_packets,
-                packets.len() - num_already_processed
-            );
+            .into(),
+            num_bundles_inserted,
+            num_packets_inserted,
+            num_bundles_dropped,
         }
     }
 
-    #[test]
-    fn test_unprocessed_transaction_storage_insert() -> Result<(), Box<dyn Error>> {
-        let keypair = Keypair::new();
-        let vote_keypair = Keypair::new();
-        let pubkey = solana_sdk::pubkey::new_rand();
-
-        let small_transfer = Packet::from_data(
-            None,
-            system_transaction::transfer(&keypair, &pubkey, 1, Hash::new_unique()),
-        )?;
-        let mut vote = Packet::from_data(
-            None,
-            new_vote_state_update_transaction(
-                VoteStateUpdate::default(),
-                Hash::new_unique(),
-                &keypair,
-                &vote_keypair,
-                &vote_keypair,
-                None,
-            ),
-        )?;
-        vote.meta_mut().flags.set(PacketFlags::SIMPLE_VOTE_TX, true);
-        let big_transfer = Packet::from_data(
-            None,
-            system_transaction::transfer(&keypair, &pubkey, 1000000, Hash::new_unique()),
-        )?;
-
-        for thread_type in [
-            ThreadType::Transactions,
-            ThreadType::Voting(VoteSource::Gossip),
-            ThreadType::Voting(VoteSource::Tpu),
-        ] {
-            let mut transaction_storage = UnprocessedTransactionStorage::new_transaction_storage(
-                UnprocessedPacketBatches::with_capacity(100),
-                thread_type,
+    fn push_front_unprocessed_bundles(
+        &mut self,
+        deserialized_bundles: Vec<ImmutableDeserializedBundle>,
+    ) -> InsertPacketBundlesSummary {
+        Self::insert_bundles(
+            &mut self.unprocessed_bundle_storage,
+            deserialized_bundles,
+            false,
+        )
+    }
+
+    fn push_back_cost_model_buffered_bundles(
+        &mut self,
+        deserialized_bundles: Vec<ImmutableDeserializedBundle>,
+    ) -> InsertPacketBundlesSummary {
+        Self::insert_bundles(
+            &mut self.cost_model_buffered_bundle_storage,
+            deserialized_bundles,
+            true,
+        )
+    }
+
+    fn insert_unprocessed_bundles(
+        &mut self,
+        deserialized_bundles: Vec<ImmutableDeserializedBundle>,
+        push_back: bool,
+    ) -> InsertPacketBundlesSummary {
+        Self::insert_bundles(
+            &mut self.unprocessed_bundle_storage,
+            deserialized_bundles,
+            push_back,
+        )
+    }
+
+    /// Drains bundles from the queue, sanitizes them to prepare for execution, executes them by
+    /// calling `processing_function`, then potentially rebuffer them.
+    pub fn process_bundles<F>(
+        &mut self,
+        bank: Arc<Bank>,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+        blacklisted_accounts: &HashSet<Pubkey>,
+        mut processing_function: F,
+    ) -> bool
+    where
+        F: FnMut(
+            &[(ImmutableDeserializedBundle, SanitizedBundle)],
+            &mut BundleStageLeaderMetrics,
+        ) -> Vec<Result<(), BundleExecutionError>>,
+    {
+        let sanitized_bundles = self.drain_and_sanitize_bundles(
+            bank,
+            bundle_stage_leader_metrics,
+            blacklisted_accounts,
+        );
+
+        debug!("processing {} bundles", sanitized_bundles.len());
+        let bundle_execution_results =
+            processing_function(&sanitized_bundles, bundle_stage_leader_metrics);
+
+        let mut is_slot_over = false;
+
+        let mut rebuffered_bundles = Vec::new();
+
+        sanitized_bundles
+            .into_iter()
+            .zip(bundle_execution_results.into_iter())
+            .for_each(
+                |((deserialized_bundle, sanitized_bundle), result)| match result {
+                    Ok(_) => {
+                        debug!("bundle={} executed ok", sanitized_bundle.bundle_id);
+                        // yippee
+                    }
+                    Err(BundleExecutionError::PohRecordError(e)) => {
+                        // buffer the bundle to the front of the queue to be attempted next slot
+                        debug!(
+                            "bundle={} poh record error: {e:?}",
+                            sanitized_bundle.bundle_id
+                        );
+                        rebuffered_bundles.push(deserialized_bundle);
+                        is_slot_over = true;
+                    }
+                    Err(BundleExecutionError::BankProcessingTimeLimitReached) => {
+                        // buffer the bundle to the front of the queue to be attempted next slot
+                        debug!("bundle={} bank processing done", sanitized_bundle.bundle_id);
+                        rebuffered_bundles.push(deserialized_bundle);
+                        is_slot_over = true;
+                    }
+                    Err(BundleExecutionError::TransactionFailure(e)) => {
+                        debug!(
+                            "bundle={} execution error: {:?}",
+                            sanitized_bundle.bundle_id, e
+                        );
+                        // do nothing
+                    }
+                    Err(BundleExecutionError::ExceedsCostModel) => {
+                        // cost model buffered bundles contain most recent bundles at the front of the queue
+                        debug!("bundle={} exceeds cost model", sanitized_bundle.bundle_id);
+                        self.push_back_cost_model_buffered_bundles(vec![deserialized_bundle]);
+                    }
+                    Err(BundleExecutionError::TipError(e)) => {
+                        debug!("bundle={} tip error: {}", sanitized_bundle.bundle_id, e);
+                        // Tip errors are _typically_ due to misconfiguration (except for poh record error, bank processing error, exceeds cost model)
+                        // in order to prevent buffering too many bundles, we'll just drop the bundle
+                    }
+                    Err(BundleExecutionError::LockError) => {
+                        // lock errors are irrecoverable due to malformed transactions
+                        debug!("bundle={} lock error", sanitized_bundle.bundle_id);
+                    }
+                },
             );
-            transaction_storage.insert_batch(vec![
-                ImmutableDeserializedPacket::new(small_transfer.clone())?,
-                ImmutableDeserializedPacket::new(vote.clone())?,
-                ImmutableDeserializedPacket::new(big_transfer.clone())?,
-            ]);
-            let deserialized_packets = transaction_storage
-                .iter()
-                .map(|packet| packet.immutable_section().original_packet().clone())
-                .collect_vec();
-            assert_eq!(3, deserialized_packets.len());
-            assert!(deserialized_packets.contains(&small_transfer));
-            assert!(deserialized_packets.contains(&vote));
-            assert!(deserialized_packets.contains(&big_transfer));
+
+        // rebuffered bundles are pushed onto deque in reverse order so the first bundle is at the front
+        for bundle in rebuffered_bundles.into_iter().rev() {
+            self.push_front_unprocessed_bundles(vec![bundle]);
         }
 
-        for vote_source in [VoteSource::Gossip, VoteSource::Tpu] {
-            let mut transaction_storage = UnprocessedTransactionStorage::new_vote_storage(
-                Arc::new(LatestUnprocessedVotes::new()),
-                vote_source,
+        is_slot_over
+    }
+
+    /// Drains the unprocessed_bundle_storage, converting bundle packets into SanitizedBundles
+    fn drain_and_sanitize_bundles(
+        &mut self,
+        bank: Arc<Bank>,
+        bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
+        blacklisted_accounts: &HashSet<Pubkey>,
+    ) -> Vec<(ImmutableDeserializedBundle, SanitizedBundle)> {
+        let mut error_metrics = TransactionErrorMetrics::default();
+
+        let start = Instant::now();
+
+        let mut sanitized_bundles = Vec::new();
+
+        // on new slot, drain anything that was buffered from last slot
+        if bank.slot() != self.last_update_slot {
+            sanitized_bundles.extend(
+                self.cost_model_buffered_bundle_storage
+                    .drain(..)
+                    .filter_map(|packet_bundle| {
+                        let r = packet_bundle.build_sanitized_bundle(
+                            &bank,
+                            blacklisted_accounts,
+                            &mut error_metrics,
+                        );
+                        bundle_stage_leader_metrics
+                            .bundle_stage_metrics_tracker()
+                            .increment_sanitize_transaction_result(&r);
+
+                        match r {
+                            Ok(sanitized_bundle) => Some((packet_bundle, sanitized_bundle)),
+                            Err(e) => {
+                                debug!(
+                                    "bundle id: {} error sanitizing: {}",
+                                    packet_bundle.bundle_id(),
+                                    e
+                                );
+                                None
+                            }
+                        }
+                    }),
             );
-            transaction_storage.insert_batch(vec![
-                ImmutableDeserializedPacket::new(small_transfer.clone())?,
-                ImmutableDeserializedPacket::new(vote.clone())?,
-                ImmutableDeserializedPacket::new(big_transfer.clone())?,
-            ]);
-            assert_eq!(1, transaction_storage.len());
+
+            self.last_update_slot = bank.slot();
         }
-        Ok(())
-    }
-
-    #[test]
-    fn test_prepare_packets_to_forward() {
-        solana_logger::setup();
-        let GenesisConfigInfo {
-            genesis_config,
-            mint_keypair,
-            ..
-        } = create_genesis_config(10);
-
-        let simple_transactions: Vec<Transaction> = (0..256)
-            .map(|_id| {
-                // packets are deserialized upon receiving, failed packets will not be
-                // forwarded; Therefore we need to create real packets here.
-                let key1 = Keypair::new();
-                system_transaction::transfer(
-                    &mint_keypair,
-                    &key1.pubkey(),
-                    genesis_config.rent.minimum_balance(0),
-                    genesis_config.hash(),
-                )
-            })
-            .collect_vec();
 
-        let mut packets: Vec<DeserializedPacket> = simple_transactions
-            .iter()
-            .enumerate()
-            .map(|(packets_id, transaction)| {
-                let mut p = Packet::from_data(None, transaction).unwrap();
-                p.meta_mut().port = packets_id as u16;
-                p.meta_mut().set_tracer(true);
-                DeserializedPacket::new(p).unwrap()
-            })
-            .collect_vec();
-
-        // test preparing buffered packets for forwarding
-        let test_prepareing_buffered_packets_for_forwarding =
-            |buffered_packet_batches: UnprocessedPacketBatches| -> (usize, usize, usize) {
-                let mut total_tracer_packets_in_buffer: usize = 0;
-                let mut total_packets_to_forward: usize = 0;
-                let mut total_tracer_packets_to_forward: usize = 0;
-
-                let mut unprocessed_transactions = ThreadLocalUnprocessedPackets {
-                    unprocessed_packet_batches: buffered_packet_batches,
-                    thread_type: ThreadType::Transactions,
-                };
-
-                let mut original_priority_queue = unprocessed_transactions.take_priority_queue();
-                let _ = original_priority_queue
-                    .drain_desc()
-                    .chunks(128usize)
-                    .into_iter()
-                    .flat_map(|packets_to_process| {
-                        let (_, packets_to_forward, is_tracer_packet) = unprocessed_transactions
-                            .prepare_packets_to_forward(
-                                packets_to_process,
-                                &mut total_tracer_packets_in_buffer,
-                            );
-                        total_packets_to_forward += packets_to_forward.len();
-                        total_tracer_packets_to_forward += is_tracer_packet.len();
-                        packets_to_forward
-                    })
-                    .collect::<MinMaxHeap<Arc<ImmutableDeserializedPacket>>>();
-                (
-                    total_tracer_packets_in_buffer,
-                    total_packets_to_forward,
-                    total_tracer_packets_to_forward,
-                )
-            };
+        sanitized_bundles.extend(self.unprocessed_bundle_storage.drain(..).filter_map(
+            |packet_bundle| {
+                let r = packet_bundle.build_sanitized_bundle(
+                    &bank,
+                    blacklisted_accounts,
+                    &mut error_metrics,
+                );
+                bundle_stage_leader_metrics
+                    .bundle_stage_metrics_tracker()
+                    .increment_sanitize_transaction_result(&r);
+                match r {
+                    Ok(sanitized_bundle) => Some((packet_bundle, sanitized_bundle)),
+                    Err(e) => {
+                        debug!(
+                            "bundle id: {} error sanitizing: {}",
+                            packet_bundle.bundle_id(),
+                            e
+                        );
+                        None
+                    }
+                }
+            },
+        ));
 
-        // all tracer packets are forwardable
-        {
-            let buffered_packet_batches: UnprocessedPacketBatches =
-                UnprocessedPacketBatches::from_iter(packets.clone().into_iter(), packets.len());
-            let (
-                total_tracer_packets_in_buffer,
-                total_packets_to_forward,
-                total_tracer_packets_to_forward,
-            ) = test_prepareing_buffered_packets_for_forwarding(buffered_packet_batches);
-            assert_eq!(total_tracer_packets_in_buffer, 256);
-            assert_eq!(total_packets_to_forward, 256);
-            assert_eq!(total_tracer_packets_to_forward, 256);
-        }
+        let elapsed = start.elapsed().as_micros();
+        bundle_stage_leader_metrics
+            .bundle_stage_metrics_tracker()
+            .increment_sanitize_bundle_elapsed_us(elapsed as u64);
+        bundle_stage_leader_metrics
+            .leader_slot_metrics_tracker()
+            .increment_transactions_from_packets_us(elapsed as u64);
 
-        // some packets are forwarded
-        {
-            let num_already_forwarded = 16;
-            for packet in &mut packets[0..num_already_forwarded] {
-                packet.forwarded = true;
-            }
-            let buffered_packet_batches: UnprocessedPacketBatches =
-                UnprocessedPacketBatches::from_iter(packets.clone().into_iter(), packets.len());
-            let (
-                total_tracer_packets_in_buffer,
-                total_packets_to_forward,
-                total_tracer_packets_to_forward,
-            ) = test_prepareing_buffered_packets_for_forwarding(buffered_packet_batches);
-            assert_eq!(total_tracer_packets_in_buffer, 256);
-            assert_eq!(total_packets_to_forward, 256 - num_already_forwarded);
-            assert_eq!(total_tracer_packets_to_forward, 256 - num_already_forwarded);
-        }
+        bundle_stage_leader_metrics
+            .leader_slot_metrics_tracker()
+            .accumulate_transaction_errors(&error_metrics);
 
-        // all packets are forwarded
-        {
-            for packet in &mut packets {
-                packet.forwarded = true;
-            }
-            let buffered_packet_batches: UnprocessedPacketBatches =
-                UnprocessedPacketBatches::from_iter(packets.clone().into_iter(), packets.len());
-            let (
-                total_tracer_packets_in_buffer,
-                total_packets_to_forward,
-                total_tracer_packets_to_forward,
-            ) = test_prepareing_buffered_packets_for_forwarding(buffered_packet_batches);
-            assert_eq!(total_tracer_packets_in_buffer, 256);
-            assert_eq!(total_packets_to_forward, 0);
-            assert_eq!(total_tracer_packets_to_forward, 0);
-        }
+        sanitized_bundles
     }
 }
diff --git a/core/src/validator.rs b/core/src/validator.rs
index 246f09a89b..44ad43a7bb 100644
--- a/core/src/validator.rs
+++ b/core/src/validator.rs
@@ -13,6 +13,7 @@ use {
         consensus::{reconcile_blockstore_roots_with_external_source, ExternalRootSource, Tower},
         ledger_metric_report_service::LedgerMetricReportService,
         poh_timing_report_service::PohTimingReportService,
+        proxy::{block_engine_stage::BlockEngineConfig, relayer_stage::RelayerConfig},
         rewards_recorder_service::{RewardsRecorderSender, RewardsRecorderService},
         sample_performance_service::SamplePerformanceService,
         serve_repair::ServeRepair,
@@ -23,6 +24,7 @@ use {
         system_monitor_service::{
             verify_net_stats_access, SystemMonitorService, SystemMonitorStatsReportConfig,
         },
+        tip_manager::TipManagerConfig,
         tower_storage::TowerStorage,
         tpu::{Tpu, TpuSockets, DEFAULT_TPU_COALESCE},
         tvu::{Tvu, TvuConfig, TvuSockets},
@@ -97,6 +99,10 @@ use {
             self, clean_orphaned_account_snapshot_dirs, move_and_async_delete_path_contents,
         },
     },
+    solana_runtime_plugin::{
+        runtime_plugin_admin_rpc_service::RuntimePluginManagerRpcRequest,
+        runtime_plugin_service::RuntimePluginService,
+    },
     solana_sdk::{
         clock::Slot,
         epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET,
@@ -117,7 +123,7 @@ use {
         path::{Path, PathBuf},
         sync::{
             atomic::{AtomicBool, AtomicU64, Ordering},
-            Arc, RwLock,
+            Arc, Mutex, RwLock,
         },
         thread::{sleep, Builder, JoinHandle},
         time::{Duration, Instant},
@@ -250,6 +256,12 @@ pub struct ValidatorConfig {
     pub block_verification_method: BlockVerificationMethod,
     pub block_production_method: BlockProductionMethod,
     pub generator_config: Option<GeneratorConfig>,
+    pub relayer_config: Arc<Mutex<RelayerConfig>>,
+    pub block_engine_config: Arc<Mutex<BlockEngineConfig>>,
+    // Using Option inside RwLock is ugly, but only convenient way to allow toggle on/off
+    pub shred_receiver_address: Arc<RwLock<Option<SocketAddr>>>,
+    pub tip_manager_config: TipManagerConfig,
+    pub preallocated_bundle_cost: u64,
 }
 
 impl Default for ValidatorConfig {
@@ -317,6 +329,11 @@ impl Default for ValidatorConfig {
             block_verification_method: BlockVerificationMethod::default(),
             block_production_method: BlockProductionMethod::default(),
             generator_config: None,
+            relayer_config: Arc::new(Mutex::new(RelayerConfig::default())),
+            block_engine_config: Arc::new(Mutex::new(BlockEngineConfig::default())),
+            shred_receiver_address: Arc::new(RwLock::new(None)),
+            tip_manager_config: TipManagerConfig::default(),
+            preallocated_bundle_cost: u64::default(),
         }
     }
 }
@@ -476,6 +493,10 @@ impl Validator {
         tpu_connection_pool_size: usize,
         tpu_enable_udp: bool,
         admin_rpc_service_post_init: Arc<RwLock<Option<AdminRpcRequestMetadataPostInit>>>,
+        runtime_plugin_configs_and_request_rx: Option<(
+            Vec<PathBuf>,
+            Receiver<RuntimePluginManagerRpcRequest>,
+        )>,
     ) -> Result<Self, String> {
         let id = identity_keypair.pubkey();
         assert_eq!(&id, node.info.pubkey());
@@ -489,10 +510,9 @@ impl Validator {
             }
         }
 
-        let mut bank_notification_senders = Vec::new();
-
         let exit = Arc::new(AtomicBool::new(false));
 
+        let mut bank_notification_senders = Vec::new();
         let geyser_plugin_service =
             if let Some(geyser_plugin_config_files) = &config.on_start_geyser_plugin_config_files {
                 let (confirmed_bank_sender, confirmed_bank_receiver) = unbounded();
@@ -838,8 +858,8 @@ impl Validator {
                 None
             };
 
-        let mut block_commitment_cache = BlockCommitmentCache::default();
         let bank_forks_guard = bank_forks.read().unwrap();
+        let mut block_commitment_cache = BlockCommitmentCache::default();
         block_commitment_cache.initialize_slots(
             bank_forks_guard.working_bank().slot(),
             bank_forks_guard.root(),
@@ -862,6 +882,17 @@ impl Validator {
             None,
         ));
 
+        if let Some((runtime_plugin_configs, request_rx)) = runtime_plugin_configs_and_request_rx {
+            RuntimePluginService::start(
+                &runtime_plugin_configs,
+                request_rx,
+                bank_forks.clone(),
+                block_commitment_cache.clone(),
+                exit.clone(),
+            )
+            .map_err(|e| format!("Failed to start runtime plugin service: {e:?}"))?;
+        }
+
         let max_slots = Arc::new(MaxSlots::default());
         let (completed_data_sets_sender, completed_data_sets_receiver) =
             bounded(MAX_COMPLETED_DATA_SETS_IN_CHANNEL);
@@ -1062,6 +1093,9 @@ impl Validator {
             cluster_info: cluster_info.clone(),
             vote_account: *vote_account,
             repair_whitelist: config.repair_whitelist.clone(),
+            block_engine_config: config.block_engine_config.clone(),
+            relayer_config: config.relayer_config.clone(),
+            shred_receiver_address: config.shred_receiver_address.clone(),
         });
 
         let waited_for_supermajority = match wait_for_supermajority(
@@ -1180,6 +1214,7 @@ impl Validator {
             &connection_cache,
             &prioritization_fee_cache,
             banking_tracer.clone(),
+            config.shred_receiver_address.clone(),
         )?;
 
         let tpu = Tpu::new(
@@ -1215,12 +1250,17 @@ impl Validator {
             &identity_keypair,
             config.runtime_config.log_messages_bytes_limit,
             &staked_nodes,
+            config.block_engine_config.clone(),
+            config.relayer_config.clone(),
+            config.tip_manager_config.clone(),
+            config.shred_receiver_address.clone(),
             config.staked_nodes_overrides.clone(),
             banking_tracer,
             tracer_thread,
             tpu_enable_udp,
             &prioritization_fee_cache,
             config.generator_config.clone(),
+            config.preallocated_bundle_cost,
         );
 
         datapoint_info!(
@@ -1667,6 +1707,7 @@ fn load_blockstore(
                 .map(|service| service.sender()),
             accounts_update_notifier,
             exit,
+            true,
         );
 
     // Before replay starts, set the callbacks in each of the banks in BankForks so that
@@ -2353,6 +2394,7 @@ mod tests {
             DEFAULT_TPU_CONNECTION_POOL_SIZE,
             DEFAULT_TPU_ENABLE_UDP,
             Arc::new(RwLock::new(None)),
+            None,
         )
         .expect("assume successful validator start");
         assert_eq!(
@@ -2444,7 +2486,7 @@ mod tests {
                     Arc::new(RwLock::new(vec![Arc::new(vote_account_keypair)])),
                     vec![LegacyContactInfo::try_from(&leader_node.info).unwrap()],
                     &config,
-                    true, // should_check_duplicate_instance.
+                    true, // should_check_duplicate_instance
                     None, // rpc_to_plugin_manager_receiver
                     Arc::new(RwLock::new(ValidatorStartProgress::default())),
                     SocketAddrSpace::Unspecified,
@@ -2452,6 +2494,7 @@ mod tests {
                     DEFAULT_TPU_CONNECTION_POOL_SIZE,
                     DEFAULT_TPU_ENABLE_UDP,
                     Arc::new(RwLock::new(None)),
+                    None,
                 )
                 .expect("assume successful validator start")
             })
diff --git a/core/tests/epoch_accounts_hash.rs b/core/tests/epoch_accounts_hash.rs
index bc9638a16c..72173a859a 100755
--- a/core/tests/epoch_accounts_hash.rs
+++ b/core/tests/epoch_accounts_hash.rs
@@ -428,6 +428,7 @@ fn test_snapshots_have_expected_epoch_accounts_hash() {
                 if let Some(full_snapshot_archive_info) =
                     snapshot_utils::get_highest_full_snapshot_archive_info(
                         &snapshot_config.full_snapshot_archives_dir,
+                        None,
                     )
                 {
                     if full_snapshot_archive_info.slot() == bank.slot() {
@@ -545,6 +546,7 @@ fn test_background_services_request_handling_for_epoch_accounts_hash() {
             info!("Taking full snapshot...");
             while snapshot_utils::get_highest_full_snapshot_archive_slot(
                 &snapshot_config.full_snapshot_archives_dir,
+                None,
             ) != Some(bank.slot())
             {
                 trace!("waiting for full snapshot...");
diff --git a/core/tests/snapshots.rs b/core/tests/snapshots.rs
index 10755d1335..0d3fe89d21 100644
--- a/core/tests/snapshots.rs
+++ b/core/tests/snapshots.rs
@@ -540,6 +540,7 @@ fn test_concurrent_snapshot_packaging(
                 // Wait until the package has been archived by SnapshotPackagerService
                 while snapshot_utils::get_highest_full_snapshot_archive_slot(
                     &full_snapshot_archives_dir,
+                    None,
                 )
                 .is_none()
                 {
@@ -898,6 +899,7 @@ fn restore_from_snapshots_and_check_banks_are_equal(
         Some(ACCOUNTS_DB_CONFIG_FOR_TESTING),
         None,
         &Arc::default(),
+        None,
     )?;
     deserialized_bank.wait_for_initial_accounts_hash_verification_completed_for_tests();
 
@@ -1063,6 +1065,7 @@ fn test_snapshots_with_background_services(
                 &snapshot_test_config
                     .snapshot_config
                     .full_snapshot_archives_dir,
+                None,
             ) != Some(slot)
             {
                 assert!(
@@ -1081,6 +1084,7 @@ fn test_snapshots_with_background_services(
                     .snapshot_config
                     .incremental_snapshot_archives_dir,
                 last_full_snapshot_slot.unwrap(),
+                None,
             ) != Some(slot)
             {
                 assert!(
@@ -1118,6 +1122,7 @@ fn test_snapshots_with_background_services(
         Some(ACCOUNTS_DB_CONFIG_FOR_TESTING),
         None,
         &exit,
+        None,
     )
     .unwrap();
     deserialized_bank.wait_for_initial_accounts_hash_verification_completed_for_tests();
diff --git a/deploy_programs b/deploy_programs
new file mode 100755
index 0000000000..cbdf837e92
--- /dev/null
+++ b/deploy_programs
@@ -0,0 +1,17 @@
+#!/usr/bin/env sh
+# Deploys the tip payment and tip distribution programs on local validator at predetermined address
+set -eux
+
+WALLET_LOCATION=~/.config/solana/id.json
+
+# build this solana binary to ensure we're using a version compatible with the validator
+cargo b --release --bin solana
+
+./target/release/solana airdrop -ul 1000 $WALLET_LOCATION
+
+(cd jito-programs/tip-payment && anchor build)
+
+# NOTE: make sure the declare_id! is set correctly in the programs
+# Also, || true to make sure if fails the first time around, tip_payment can still be deployed
+RUST_INFO=trace ./target/release/solana deploy --keypair $WALLET_LOCATION -ul ./jito-programs/tip-payment/target/deploy/tip_distribution.so ./jito-programs/tip-payment/dev/dev_tip_distribution.json || true
+RUST_INFO=trace ./target/release/solana deploy --keypair $WALLET_LOCATION -ul ./jito-programs/tip-payment/target/deploy/tip_payment.so  ./jito-programs/tip-payment/dev/dev_tip_payment.json
diff --git a/dev/Dockerfile b/dev/Dockerfile
new file mode 100644
index 0000000000..bab9a1c02f
--- /dev/null
+++ b/dev/Dockerfile
@@ -0,0 +1,48 @@
+FROM rust:1.64-slim-bullseye as builder
+
+# Add Google Protocol Buffers for Libra's metrics library.
+ENV PROTOC_VERSION 3.8.0
+ENV PROTOC_ZIP protoc-$PROTOC_VERSION-linux-x86_64.zip
+
+RUN set -x \
+ && apt update \
+ && apt install -y \
+      clang \
+      cmake \
+      libudev-dev \
+      make \
+      unzip \
+      libssl-dev \
+      pkg-config \
+      zlib1g-dev \
+      curl \
+ && rustup component add rustfmt \
+ && rustup component add clippy \
+ && rustc --version \
+ && cargo --version \
+ && curl -OL https://github.com/google/protobuf/releases/download/v$PROTOC_VERSION/$PROTOC_ZIP \
+ && unzip -o $PROTOC_ZIP -d /usr/local bin/protoc \
+ && unzip -o $PROTOC_ZIP -d /usr/local include/* \
+ && rm -f $PROTOC_ZIP
+
+
+WORKDIR /solana
+COPY . .
+RUN mkdir -p docker-output
+
+ARG ci_commit
+# NOTE: Keep this here before build since variable is referenced during CI build step.
+ENV CI_COMMIT=$ci_commit
+
+ARG debug
+
+# Uses docker buildkit to cache the image.
+# /usr/local/cargo/git needed for crossbeam patch
+RUN --mount=type=cache,mode=0777,target=/solana/target \
+    --mount=type=cache,mode=0777,target=/usr/local/cargo/registry \
+    --mount=type=cache,mode=0777,target=/usr/local/cargo/git \
+    if [ "$debug" = "false" ] ; then \
+      ./cargo stable build --release && cp target/release/solana* ./docker-output; \
+    else \
+      RUSTFLAGS='-g -C force-frame-pointers=yes' ./cargo stable build --release && cp target/release/solana* ./docker-output; \
+    fi
diff --git a/entry/src/entry.rs b/entry/src/entry.rs
index 0551abe02a..71bdbbaca0 100644
--- a/entry/src/entry.rs
+++ b/entry/src/entry.rs
@@ -232,7 +232,7 @@ pub fn hash_transactions(transactions: &[VersionedTransaction]) -> Hash {
         .iter()
         .flat_map(|tx| tx.signatures.iter())
         .collect();
-    let merkle_tree = MerkleTree::new(&signatures);
+    let merkle_tree = MerkleTree::new(&signatures, false);
     if let Some(root_hash) = merkle_tree.get_root() {
         *root_hash
     } else {
diff --git a/entry/src/poh.rs b/entry/src/poh.rs
index 573b1ab606..00c21c7586 100644
--- a/entry/src/poh.rs
+++ b/entry/src/poh.rs
@@ -72,19 +72,30 @@ impl Poh {
     }
 
     pub fn record(&mut self, mixin: Hash) -> Option<PohEntry> {
-        if self.remaining_hashes == 1 {
+        let entries = self.record_bundle(&[mixin]);
+        entries.unwrap_or_default().pop()
+    }
+
+    pub fn record_bundle(&mut self, mixins: &[Hash]) -> Option<Vec<PohEntry>> {
+        if self.remaining_hashes <= mixins.len() as u64 {
             return None; // Caller needs to `tick()` first
         }
 
-        self.hash = hashv(&[self.hash.as_ref(), mixin.as_ref()]);
-        let num_hashes = self.num_hashes + 1;
-        self.num_hashes = 0;
-        self.remaining_hashes -= 1;
+        let entries = mixins
+            .iter()
+            .map(|m| {
+                self.hash = hashv(&[self.hash.as_ref(), m.as_ref()]);
+                let num_hashes = self.num_hashes + 1;
+                self.num_hashes = 0;
+                self.remaining_hashes -= 1;
+                PohEntry {
+                    num_hashes,
+                    hash: self.hash,
+                }
+            })
+            .collect();
 
-        Some(PohEntry {
-            num_hashes,
-            hash: self.hash,
-        })
+        Some(entries)
     }
 
     pub fn tick(&mut self) -> Option<PohEntry> {
diff --git a/f b/f
new file mode 100755
index 0000000000..e5fe635508
--- /dev/null
+++ b/f
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+# Builds jito-solana in a docker container.
+# Useful for running on machines that might not have cargo installed but can run docker (Flatcar Linux).
+# run `./f true` to compile with debug flags
+
+set -eux
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
+
+GIT_SHA="$(git rev-parse --short HEAD)"
+
+echo "Git hash: $GIT_SHA"
+
+DEBUG_FLAGS=${1-false}
+
+DOCKER_BUILDKIT=1 docker build \
+  --build-arg debug=$DEBUG_FLAGS \
+  --build-arg ci_commit=$GIT_SHA \
+  -t jitolabs/build-solana \
+  -f dev/Dockerfile . \
+  --progress=plain
+
+# Creates a temporary container, copies solana-validator built inside container there and
+# removes the temporary container.
+docker rm temp || true
+docker container create --name temp jitolabs/build-solana
+mkdir -p $SCRIPT_DIR/docker-output
+# Outputs the solana-validator binary to $SOLANA/docker-output/solana-validator
+docker container cp temp:/solana/docker-output $SCRIPT_DIR/
+docker rm temp
diff --git a/fetch-spl.sh b/fetch-spl.sh
index bb8e84ebb2..8fe111a60d 100755
--- a/fetch-spl.sh
+++ b/fetch-spl.sh
@@ -13,8 +13,24 @@ fetch_program() {
   declare version=$2
   declare address=$3
   declare loader=$4
+  declare repo=$5
 
-  declare so=spl_$name-$version.so
+  case $repo in
+  "jito")
+    so=$name-$version.so
+    so_name="$name.so"
+    url="https://github.com/jito-foundation/jito-programs/releases/download/v$version/$so_name"
+    ;;
+  "solana")
+    so=spl_$name-$version.so
+    so_name="spl_${name//-/_}.so"
+    url="https://github.com/solana-labs/solana-program-library/releases/download/$name-v$version/$so_name"
+    ;;
+  *)
+    echo "Unsupported repo: $repo"
+    return 1
+    ;;
+  esac
 
   if [[ $loader == "$upgradeableLoader" ]]; then
     genesis_args+=(--upgradeable-program "$address" "$loader" "$so" none)
@@ -30,12 +46,11 @@ fetch_program() {
     cp ~/.cache/solana-spl/"$so" "$so"
   else
     echo "Downloading $name $version"
-    so_name="spl_${name//-/_}.so"
     (
       set -x
       curl -L --retry 5 --retry-delay 2 --retry-connrefused \
         -o "$so" \
-        "https://github.com/solana-labs/solana-program-library/releases/download/$name-v$version/$so_name"
+        "$url"
     )
 
     mkdir -p ~/.cache/solana-spl
@@ -44,19 +59,25 @@ fetch_program() {
 
 }
 
-fetch_program token 3.5.0 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA BPFLoader2111111111111111111111111111111111
-fetch_program token-2022 0.9.0 TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb BPFLoaderUpgradeab1e11111111111111111111111
-fetch_program memo  1.0.0 Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo BPFLoader1111111111111111111111111111111111
-fetch_program memo  3.0.0 MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr BPFLoader2111111111111111111111111111111111
-fetch_program associated-token-account 1.1.2 ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL BPFLoader2111111111111111111111111111111111
-fetch_program feature-proposal 1.0.0 Feat1YXHhH6t1juaWF74WLcfv4XoNocjXA6sPWHNgAse BPFLoader2111111111111111111111111111111111
+fetch_program token 3.5.0 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA BPFLoader2111111111111111111111111111111111 solana
+fetch_program token-2022 0.9.0 TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb BPFLoaderUpgradeab1e11111111111111111111111 solana
+fetch_program memo  1.0.0 Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo BPFLoader1111111111111111111111111111111111 solana
+fetch_program memo  3.0.0 MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr BPFLoader2111111111111111111111111111111111 solana
+fetch_program associated-token-account 1.1.2 ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL BPFLoader2111111111111111111111111111111111 solana
+fetch_program feature-proposal 1.0.0 Feat1YXHhH6t1juaWF74WLcfv4XoNocjXA6sPWHNgAse BPFLoader2111111111111111111111111111111111 solana
+# jito programs
+fetch_program jito_tip_payment 0.1.3 T1pyyaTNZsKv2WcRAB8oVnk93mLJw2XzjtVYqCsaHqt BPFLoaderUpgradeab1e11111111111111111111111 jito
+fetch_program jito_tip_distribution 0.1.3 4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7 BPFLoaderUpgradeab1e11111111111111111111111 jito
 
-echo "${genesis_args[@]}" > spl-genesis-args.sh
+echo "${genesis_args[@]}" >spl-genesis-args.sh
 
 echo
 echo "Available SPL programs:"
 ls -l spl_*.so
 
+echo "Available Jito programs:"
+ls -l jito*.so
+
 echo
 echo "solana-genesis command-line arguments (spl-genesis-args.sh):"
 cat spl-genesis-args.sh
diff --git a/frozen-abi/macro/src/lib.rs b/frozen-abi/macro/src/lib.rs
index 8a1358b391..0c37eeb149 100644
--- a/frozen-abi/macro/src/lib.rs
+++ b/frozen-abi/macro/src/lib.rs
@@ -425,7 +425,7 @@ pub fn frozen_abi(attrs: TokenStream, item: TokenStream) -> TokenStream {
             "the required \"digest\" = ... attribute is missing.",
         )
         .to_compile_error()
-        .into()
+        .into();
     };
 
     let item = parse_macro_input!(item as Item);
diff --git a/geyser-plugin-manager/src/geyser_plugin_manager.rs b/geyser-plugin-manager/src/geyser_plugin_manager.rs
index 1e39d3df72..41f7e3eea8 100644
--- a/geyser-plugin-manager/src/geyser_plugin_manager.rs
+++ b/geyser-plugin-manager/src/geyser_plugin_manager.rs
@@ -127,15 +127,17 @@ impl GeyserPluginManager {
 
     pub(crate) fn unload_plugin(&mut self, name: &str) -> JsonRpcResult<()> {
         // Check if any plugin names match this one
-        let Some(idx) = self.plugins.iter().position(|plugin| plugin.name().eq(name)) else {
+        let Some(idx) = self
+            .plugins
+            .iter()
+            .position(|plugin| plugin.name().eq(name))
+        else {
             // If we don't find one return an error
-            return Err(
-                jsonrpc_core::error::Error {
-                    code: ErrorCode::InvalidRequest,
-                    message: String::from("The plugin you requested to unload is not loaded"),
-                    data: None,
-                }
-            )
+            return Err(jsonrpc_core::error::Error {
+                code: ErrorCode::InvalidRequest,
+                message: String::from("The plugin you requested to unload is not loaded"),
+                data: None,
+            });
         };
 
         // Unload and drop plugin and lib
@@ -149,15 +151,17 @@ impl GeyserPluginManager {
     /// Then, attempt to load a new plugin
     pub(crate) fn reload_plugin(&mut self, name: &str, config_file: &str) -> JsonRpcResult<()> {
         // Check if any plugin names match this one
-        let Some(idx) = self.plugins.iter().position(|plugin| plugin.name().eq(name)) else {
+        let Some(idx) = self
+            .plugins
+            .iter()
+            .position(|plugin| plugin.name().eq(name))
+        else {
             // If we don't find one return an error
-            return Err(
-                jsonrpc_core::error::Error {
-                    code: ErrorCode::InvalidRequest,
-                    message: String::from("The plugin you requested to reload is not loaded"),
-                    data: None,
-                }
-            )
+            return Err(jsonrpc_core::error::Error {
+                code: ErrorCode::InvalidRequest,
+                message: String::from("The plugin you requested to reload is not loaded"),
+                data: None,
+            });
         };
 
         // Unload and drop current plugin first in case plugin requires exclusive access to resource,
diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs
index 117d2092ab..443a18953a 100644
--- a/gossip/src/cluster_info.rs
+++ b/gossip/src/cluster_info.rs
@@ -542,6 +542,10 @@ impl ClusterInfo {
         *self.entrypoints.write().unwrap() = entrypoints;
     }
 
+    pub fn set_my_contact_info(&self, my_contact_info: ContactInfo) {
+        *self.my_contact_info.write().unwrap() = my_contact_info;
+    }
+
     pub fn save_contact_info(&self) {
         let nodes = {
             let entrypoint_gossip_addrs = self
diff --git a/jito-programs b/jito-programs
new file mode 160000
index 0000000000..180be58caf
--- /dev/null
+++ b/jito-programs
@@ -0,0 +1 @@
+Subproject commit 180be58cafcbbe3dac66bbe6428579d58d58f683
diff --git a/jito-protos/Cargo.toml b/jito-protos/Cargo.toml
new file mode 100644
index 0000000000..008767e3ba
--- /dev/null
+++ b/jito-protos/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "jito-protos"
+version = { workspace = true }
+edition = { workspace = true }
+license = { workspace = true }
+
+[dependencies]
+bytes = { workspace = true }
+prost = { workspace = true }
+prost-types = { workspace = true }
+tonic = { workspace = true }
+
+[build-dependencies]
+tonic-build = { workspace = true }
+
+# windows users should install the protobuf compiler manually and set the PROTOC
+# envar to point to the installed binary
+[target."cfg(not(windows))".build-dependencies]
+protobuf-src = { workspace = true }
diff --git a/jito-protos/build.rs b/jito-protos/build.rs
new file mode 100644
index 0000000000..37f5a261a4
--- /dev/null
+++ b/jito-protos/build.rs
@@ -0,0 +1,38 @@
+use tonic_build::configure;
+
+fn main() -> Result<(), std::io::Error> {
+    const PROTOC_ENVAR: &str = "PROTOC";
+    if std::env::var(PROTOC_ENVAR).is_err() {
+        #[cfg(not(windows))]
+        std::env::set_var(PROTOC_ENVAR, protobuf_src::protoc());
+    }
+
+    let proto_base_path = std::path::PathBuf::from("protos");
+    let proto_files = [
+        "auth.proto",
+        "block_engine.proto",
+        "bundle.proto",
+        "packet.proto",
+        "relayer.proto",
+        "shared.proto",
+    ];
+    let mut protos = Vec::new();
+    for proto_file in &proto_files {
+        let proto = proto_base_path.join(proto_file);
+        println!("cargo::rerun-if-changed={}", proto.display());
+        protos.push(proto);
+    }
+
+    configure()
+        .build_client(true)
+        .build_server(false)
+        .type_attribute(
+            "TransactionErrorType",
+            "#[cfg_attr(test, derive(enum_iterator::Sequence))]",
+        )
+        .type_attribute(
+            "InstructionErrorType",
+            "#[cfg_attr(test, derive(enum_iterator::Sequence))]",
+        )
+        .compile(&protos, &[proto_base_path])
+}
diff --git a/jito-protos/protos b/jito-protos/protos
new file mode 160000
index 0000000000..05d210980f
--- /dev/null
+++ b/jito-protos/protos
@@ -0,0 +1 @@
+Subproject commit 05d210980f34a7c974d7ed1a4dbcb2ce1fca00b3
diff --git a/jito-protos/src/lib.rs b/jito-protos/src/lib.rs
new file mode 100644
index 0000000000..cf630c53d2
--- /dev/null
+++ b/jito-protos/src/lib.rs
@@ -0,0 +1,25 @@
+pub mod proto {
+    pub mod auth {
+        tonic::include_proto!("auth");
+    }
+
+    pub mod block_engine {
+        tonic::include_proto!("block_engine");
+    }
+
+    pub mod bundle {
+        tonic::include_proto!("bundle");
+    }
+
+    pub mod packet {
+        tonic::include_proto!("packet");
+    }
+
+    pub mod relayer {
+        tonic::include_proto!("relayer");
+    }
+
+    pub mod shared {
+        tonic::include_proto!("shared");
+    }
+}
diff --git a/ledger-tool/src/ledger_utils.rs b/ledger-tool/src/ledger_utils.rs
index 6e12f300cb..8d0c484948 100644
--- a/ledger-tool/src/ledger_utils.rs
+++ b/ledger-tool/src/ledger_utils.rs
@@ -74,6 +74,7 @@ pub fn load_and_process_ledger(
     process_options: ProcessOptions,
     snapshot_archive_path: Option<PathBuf>,
     incremental_snapshot_archive_path: Option<PathBuf>,
+    ignore_halt_at_slot_for_snapshot_loading: bool,
 ) -> Result<(Arc<RwLock<BankForks>>, Option<StartingSnapshotHashes>), BlockstoreProcessorError> {
     let bank_snapshots_dir = blockstore
         .ledger_path()
@@ -83,6 +84,12 @@ pub fn load_and_process_ledger(
             "snapshot.ledger-tool"
         });
 
+    let snapshot_halt_at_slot = if ignore_halt_at_slot_for_snapshot_loading {
+        None
+    } else {
+        process_options.halt_at_slot
+    };
+
     let mut starting_slot = 0; // default start check with genesis
     let snapshot_config = if arg_matches.is_present("no_snapshot") {
         None
@@ -91,13 +98,15 @@ pub fn load_and_process_ledger(
             snapshot_archive_path.unwrap_or_else(|| blockstore.ledger_path().to_path_buf());
         let incremental_snapshot_archives_dir =
             incremental_snapshot_archive_path.unwrap_or_else(|| full_snapshot_archives_dir.clone());
-        if let Some(full_snapshot_slot) =
-            snapshot_utils::get_highest_full_snapshot_archive_slot(&full_snapshot_archives_dir)
-        {
+        if let Some(full_snapshot_slot) = snapshot_utils::get_highest_full_snapshot_archive_slot(
+            &full_snapshot_archives_dir,
+            snapshot_halt_at_slot,
+        ) {
             let incremental_snapshot_slot =
                 snapshot_utils::get_highest_incremental_snapshot_archive_slot(
                     &incremental_snapshot_archives_dir,
                     full_snapshot_slot,
+                    snapshot_halt_at_slot,
                 )
                 .unwrap_or_default();
             starting_slot = std::cmp::max(full_snapshot_slot, incremental_snapshot_slot);
@@ -242,6 +251,7 @@ pub fn load_and_process_ledger(
             None, // Maybe support this later, though
             accounts_update_notifier,
             &Arc::default(),
+            ignore_halt_at_slot_for_snapshot_loading,
         );
     let block_verification_method = value_t!(
         arg_matches,
diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs
index fa4e09ef42..209627058f 100644
--- a/ledger-tool/src/main.rs
+++ b/ledger-tool/src/main.rs
@@ -990,10 +990,11 @@ fn get_latest_optimistic_slots(
     let Some(latest_slot) = blockstore
         .get_latest_optimistic_slots(1)
         .expect("get_latest_optimistic_slots() failed")
-        .pop() else {
-            eprintln!("Blockstore does not contain any optimistically confirmed slots");
-            return vec![];
-        };
+        .pop()
+    else {
+        eprintln!("Blockstore does not contain any optimistically confirmed slots");
+        return vec![];
+    };
     let latest_slot = latest_slot.0;
 
     let slot_iter = AncestorIterator::new_inclusive(latest_slot, blockstore).map(|slot| {
@@ -2276,6 +2277,7 @@ fn main() {
                     process_options,
                     snapshot_archive_path,
                     incremental_snapshot_archive_path,
+                    true,
                 ) {
                     Ok((bank_forks, ..)) => {
                         println!(
@@ -2368,6 +2370,7 @@ fn main() {
                     process_options,
                     snapshot_archive_path,
                     incremental_snapshot_archive_path,
+                    true,
                 ) {
                     Ok((bank_forks, ..)) => {
                         println!("{}", &bank_forks.read().unwrap().working_bank().hash());
@@ -2606,6 +2609,7 @@ fn main() {
                     process_options,
                     snapshot_archive_path,
                     incremental_snapshot_archive_path,
+                    true,
                 )
                 .unwrap_or_else(|err| {
                     eprintln!("Ledger verification failed: {err:?}");
@@ -2650,6 +2654,7 @@ fn main() {
                     process_options,
                     snapshot_archive_path,
                     incremental_snapshot_archive_path,
+                    true,
                 ) {
                     Ok((bank_forks, ..)) => {
                         let dot = graph_forks(&bank_forks.read().unwrap(), &graph_config);
@@ -2784,6 +2789,21 @@ fn main() {
                     exit(1);
                 }
 
+                if let Ok(metas) = blockstore.slot_meta_iterator(0) {
+                    let slots: Vec<_> = metas.map(|(slot, _)| slot).collect();
+                    if slots.is_empty() {
+                        eprintln!("Ledger is empty, can't create snapshot");
+                        exit(1);
+                    } else {
+                        let first = slots.first().unwrap();
+                        let last = slots.last().unwrap_or(first);
+                        if first > &snapshot_slot || &snapshot_slot > last {
+                            eprintln!("Slot {} is out of bounds of ledger [{}, {}], cannot create snapshot", &snapshot_slot, first, last);
+                            exit(1);
+                        }
+                    }
+                }
+
                 let ending_slot = if is_minimized {
                     let ending_slot = value_t_or_exit!(arg_matches, "ending_slot", Slot);
                     if ending_slot <= snapshot_slot {
@@ -2827,6 +2847,7 @@ fn main() {
                     },
                     snapshot_archive_path,
                     incremental_snapshot_archive_path,
+                    false, // want to load snapshots <= halt_at_slot
                 ) {
                     Ok((bank_forks, starting_snapshot_hashes)) => {
                         let mut bank = bank_forks
@@ -3191,6 +3212,7 @@ fn main() {
                     process_options,
                     snapshot_archive_path,
                     incremental_snapshot_archive_path,
+                    true,
                 )
                 .unwrap_or_else(|err| {
                     eprintln!("Failed to load ledger: {err:?}");
@@ -3280,6 +3302,7 @@ fn main() {
                     process_options,
                     snapshot_archive_path,
                     incremental_snapshot_archive_path,
+                    true,
                 ) {
                     Ok((bank_forks, ..)) => {
                         let bank_forks = bank_forks.read().unwrap();
diff --git a/ledger-tool/src/program.rs b/ledger-tool/src/program.rs
index 359ca60aae..6329799130 100644
--- a/ledger-tool/src/program.rs
+++ b/ledger-tool/src/program.rs
@@ -124,6 +124,7 @@ fn load_blockstore(ledger_path: &Path, arg_matches: &ArgMatches<'_>) -> Arc<Bank
         process_options,
         snapshot_archive_path,
         incremental_snapshot_archive_path,
+        true,
     )
     .unwrap_or_else(|err| {
         eprintln!("Ledger loading failed: {err:?}");
diff --git a/ledger/src/bank_forks_utils.rs b/ledger/src/bank_forks_utils.rs
index 2062c96f5d..e72f706301 100644
--- a/ledger/src/bank_forks_utils.rs
+++ b/ledger/src/bank_forks_utils.rs
@@ -65,6 +65,7 @@ pub fn load(
         entry_notification_sender,
         accounts_update_notifier,
         exit,
+        true,
     );
 
     blockstore_processor::process_blockstore_from_root(
@@ -92,6 +93,7 @@ pub fn load_bank_forks(
     entry_notification_sender: Option<&EntryNotifierSender>,
     accounts_update_notifier: Option<AccountsUpdateNotifier>,
     exit: &Arc<AtomicBool>,
+    ignore_halt_at_slot_for_snapshot_loading: bool,
 ) -> (
     Arc<RwLock<BankForks>>,
     LeaderScheduleCache,
@@ -105,8 +107,15 @@ pub fn load_bank_forks(
         fs::create_dir_all(&snapshot_config.bank_snapshots_dir)
             .expect("Couldn't create snapshot directory");
 
+        let halt_at_slot = if ignore_halt_at_slot_for_snapshot_loading {
+            None
+        } else {
+            process_options.halt_at_slot
+        };
+
         if snapshot_utils::get_highest_full_snapshot_archive_info(
             &snapshot_config.full_snapshot_archives_dir,
+            halt_at_slot,
         )
         .is_some()
         {
@@ -124,12 +133,19 @@ pub fn load_bank_forks(
     };
 
     let (bank_forks, starting_snapshot_hashes) = if snapshot_present {
+        let mut process_options = process_options.clone();
+        process_options.halt_at_slot = if ignore_halt_at_slot_for_snapshot_loading {
+            None
+        } else {
+            process_options.halt_at_slot
+        };
+
         bank_forks_from_snapshot(
             genesis_config,
             account_paths,
             shrink_paths,
             snapshot_config.as_ref().unwrap(),
-            process_options,
+            &process_options,
             accounts_update_notifier,
             exit,
         )
@@ -189,7 +205,7 @@ pub fn load_bank_forks(
 }
 
 #[allow(clippy::too_many_arguments)]
-fn bank_forks_from_snapshot(
+pub fn bank_forks_from_snapshot(
     genesis_config: &GenesisConfig,
     account_paths: Vec<PathBuf>,
     shrink_paths: Option<Vec<PathBuf>>,
@@ -230,6 +246,7 @@ fn bank_forks_from_snapshot(
             process_options.accounts_db_config.clone(),
             accounts_update_notifier,
             exit,
+            process_options.halt_at_slot,
         )
         .unwrap_or_else(|err| {
             error!("Failed to load bank from snapshot archives: {err}");
diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs
index 42588b3a9d..6875a9ae64 100644
--- a/ledger/src/blockstore_processor.rs
+++ b/ledger/src/blockstore_processor.rs
@@ -150,7 +150,7 @@ fn execute_batch(
     let mut mint_decimals: HashMap<Pubkey, u8> = HashMap::new();
 
     let pre_token_balances = if record_token_balances {
-        collect_token_balances(bank, batch, &mut mint_decimals)
+        collect_token_balances(bank, batch, &mut mint_decimals, None)
     } else {
         vec![]
     };
@@ -188,7 +188,7 @@ fn execute_batch(
     if let Some(transaction_status_sender) = transaction_status_sender {
         let transactions = batch.sanitized_transactions().to_vec();
         let post_token_balances = if record_token_balances {
-            collect_token_balances(bank, batch, &mut mint_decimals)
+            collect_token_balances(bank, batch, &mut mint_decimals, None)
         } else {
             vec![]
         };
@@ -684,6 +684,7 @@ pub fn test_process_blockstore(
         None,
         None,
         exit,
+        true,
     );
 
     process_blockstore_from_root(
diff --git a/ledger/src/token_balances.rs b/ledger/src/token_balances.rs
index 5dc368b103..c2b7a04840 100644
--- a/ledger/src/token_balances.rs
+++ b/ledger/src/token_balances.rs
@@ -4,7 +4,9 @@ use {
     },
     solana_measure::measure::Measure,
     solana_metrics::datapoint_debug,
-    solana_runtime::{bank::Bank, transaction_batch::TransactionBatch},
+    solana_runtime::{
+        account_overrides::AccountOverrides, bank::Bank, transaction_batch::TransactionBatch,
+    },
     solana_sdk::{account::ReadableAccount, pubkey::Pubkey},
     solana_transaction_status::{
         token_balances::TransactionTokenBalances, TransactionTokenBalance,
@@ -38,6 +40,7 @@ pub fn collect_token_balances(
     bank: &Bank,
     batch: &TransactionBatch,
     mint_decimals: &mut HashMap<Pubkey, u8>,
+    cached_accounts: Option<&AccountOverrides>,
 ) -> TransactionTokenBalances {
     let mut balances: TransactionTokenBalances = vec![];
     let mut collect_time = Measure::start("collect_token_balances");
@@ -58,8 +61,12 @@ pub fn collect_token_balances(
                     ui_token_amount,
                     owner,
                     program_id,
-                }) = collect_token_balance_from_account(bank, account_id, mint_decimals)
-                {
+                }) = collect_token_balance_from_account(
+                    bank,
+                    account_id,
+                    mint_decimals,
+                    cached_accounts,
+                ) {
                     transaction_balances.push(TransactionTokenBalance {
                         account_index: index as u8,
                         mint,
@@ -92,8 +99,17 @@ fn collect_token_balance_from_account(
     bank: &Bank,
     account_id: &Pubkey,
     mint_decimals: &mut HashMap<Pubkey, u8>,
+    account_overrides: Option<&AccountOverrides>,
 ) -> Option<TokenBalanceData> {
-    let account = bank.get_account(account_id)?;
+    let account = {
+        if let Some(account_override) =
+            account_overrides.and_then(|overrides| overrides.get(account_id))
+        {
+            Some(account_override.clone())
+        } else {
+            bank.get_account(account_id)
+        }
+    }?;
 
     if !is_known_spl_token_id(account.owner()) {
         return None;
@@ -235,13 +251,13 @@ mod test {
 
         // Account is not owned by spl_token (nor does it have TokenAccount state)
         assert_eq!(
-            collect_token_balance_from_account(&bank, &account_pubkey, &mut mint_decimals),
+            collect_token_balance_from_account(&bank, &account_pubkey, &mut mint_decimals, None),
             None
         );
 
         // Mint does not have TokenAccount state
         assert_eq!(
-            collect_token_balance_from_account(&bank, &mint_pubkey, &mut mint_decimals),
+            collect_token_balance_from_account(&bank, &mint_pubkey, &mut mint_decimals, None),
             None
         );
 
@@ -250,7 +266,8 @@ mod test {
             collect_token_balance_from_account(
                 &bank,
                 &spl_token_account_pubkey,
-                &mut mint_decimals
+                &mut mint_decimals,
+                None
             ),
             Some(TokenBalanceData {
                 mint: mint_pubkey.to_string(),
@@ -267,7 +284,12 @@ mod test {
 
         // TokenAccount is not owned by known spl-token program_id
         assert_eq!(
-            collect_token_balance_from_account(&bank, &other_account_pubkey, &mut mint_decimals),
+            collect_token_balance_from_account(
+                &bank,
+                &other_account_pubkey,
+                &mut mint_decimals,
+                None
+            ),
             None
         );
 
@@ -276,7 +298,8 @@ mod test {
             collect_token_balance_from_account(
                 &bank,
                 &other_mint_account_pubkey,
-                &mut mint_decimals
+                &mut mint_decimals,
+                None
             ),
             None
         );
@@ -429,13 +452,13 @@ mod test {
 
         // Account is not owned by spl_token (nor does it have TokenAccount state)
         assert_eq!(
-            collect_token_balance_from_account(&bank, &account_pubkey, &mut mint_decimals),
+            collect_token_balance_from_account(&bank, &account_pubkey, &mut mint_decimals, None),
             None
         );
 
         // Mint does not have TokenAccount state
         assert_eq!(
-            collect_token_balance_from_account(&bank, &mint_pubkey, &mut mint_decimals),
+            collect_token_balance_from_account(&bank, &mint_pubkey, &mut mint_decimals, None),
             None
         );
 
@@ -444,7 +467,8 @@ mod test {
             collect_token_balance_from_account(
                 &bank,
                 &spl_token_account_pubkey,
-                &mut mint_decimals
+                &mut mint_decimals,
+                None
             ),
             Some(TokenBalanceData {
                 mint: mint_pubkey.to_string(),
@@ -461,7 +485,12 @@ mod test {
 
         // TokenAccount is not owned by known spl-token program_id
         assert_eq!(
-            collect_token_balance_from_account(&bank, &other_account_pubkey, &mut mint_decimals),
+            collect_token_balance_from_account(
+                &bank,
+                &other_account_pubkey,
+                &mut mint_decimals,
+                None
+            ),
             None
         );
 
@@ -470,7 +499,8 @@ mod test {
             collect_token_balance_from_account(
                 &bank,
                 &other_mint_account_pubkey,
-                &mut mint_decimals
+                &mut mint_decimals,
+                None
             ),
             None
         );
diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs
index ccf621c05c..08ccf08ce5 100644
--- a/local-cluster/src/local_cluster.rs
+++ b/local-cluster/src/local_cluster.rs
@@ -279,6 +279,7 @@ impl LocalCluster {
             DEFAULT_TPU_CONNECTION_POOL_SIZE,
             DEFAULT_TPU_ENABLE_UDP,
             Arc::new(RwLock::new(None)),
+            None,
         )
         .expect("assume successful validator start");
 
@@ -494,6 +495,7 @@ impl LocalCluster {
             DEFAULT_TPU_CONNECTION_POOL_SIZE,
             DEFAULT_TPU_ENABLE_UDP,
             Arc::new(RwLock::new(None)),
+            None,
         )
         .expect("assume successful validator start");
 
@@ -887,6 +889,7 @@ impl Cluster for LocalCluster {
             DEFAULT_TPU_CONNECTION_POOL_SIZE,
             DEFAULT_TPU_ENABLE_UDP,
             Arc::new(RwLock::new(None)),
+            None,
         )
         .expect("assume successful validator start");
         cluster_validator_info.validator = Some(restarted_node);
diff --git a/local-cluster/src/local_cluster_snapshot_utils.rs b/local-cluster/src/local_cluster_snapshot_utils.rs
index 259b9e1559..2ebd90e86e 100644
--- a/local-cluster/src/local_cluster_snapshot_utils.rs
+++ b/local-cluster/src/local_cluster_snapshot_utils.rs
@@ -90,7 +90,10 @@ impl LocalCluster {
         let timer = Instant::now();
         let next_snapshot = loop {
             if let Some(full_snapshot_archive_info) =
-                snapshot_utils::get_highest_full_snapshot_archive_info(&full_snapshot_archives_dir)
+                snapshot_utils::get_highest_full_snapshot_archive_info(
+                    &full_snapshot_archives_dir,
+                    None,
+                )
             {
                 match next_snapshot_type {
                     NextSnapshotType::FullSnapshot => {
@@ -103,6 +106,7 @@ impl LocalCluster {
                             snapshot_utils::get_highest_incremental_snapshot_archive_info(
                                 incremental_snapshot_archives_dir.as_ref().unwrap(),
                                 full_snapshot_archive_info.slot(),
+                                None,
                             )
                         {
                             if incremental_snapshot_archive_info.slot() >= last_slot {
diff --git a/local-cluster/src/validator_configs.rs b/local-cluster/src/validator_configs.rs
index d81bcb8b6b..e840795311 100644
--- a/local-cluster/src/validator_configs.rs
+++ b/local-cluster/src/validator_configs.rs
@@ -68,6 +68,11 @@ pub fn safe_clone_config(config: &ValidatorConfig) -> ValidatorConfig {
         block_verification_method: config.block_verification_method.clone(),
         block_production_method: config.block_production_method.clone(),
         generator_config: config.generator_config.clone(),
+        relayer_config: config.relayer_config.clone(),
+        block_engine_config: config.block_engine_config.clone(),
+        shred_receiver_address: config.shred_receiver_address.clone(),
+        tip_manager_config: config.tip_manager_config.clone(),
+        preallocated_bundle_cost: config.preallocated_bundle_cost,
     }
 }
 
diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs
index 0c1b073807..8bbbc1209f 100644
--- a/local-cluster/tests/local_cluster.rs
+++ b/local-cluster/tests/local_cluster.rs
@@ -855,6 +855,7 @@ fn test_incremental_snapshot_download_with_crossing_full_snapshot_interval_at_st
         validator_snapshot_test_config
             .full_snapshot_archives_dir
             .path(),
+        None,
     )
     .unwrap();
     info!(
@@ -894,6 +895,7 @@ fn test_incremental_snapshot_download_with_crossing_full_snapshot_interval_at_st
                 .incremental_snapshot_archives_dir
                 .path(),
             full_snapshot_archive.slot(),
+            None,
         )
         .unwrap();
     info!(
@@ -1036,6 +1038,7 @@ fn test_incremental_snapshot_download_with_crossing_full_snapshot_interval_at_st
             validator_snapshot_test_config
                 .full_snapshot_archives_dir
                 .path(),
+            None,
         )
         .unwrap();
 
@@ -1102,6 +1105,7 @@ fn test_incremental_snapshot_download_with_crossing_full_snapshot_interval_at_st
             validator_snapshot_test_config
                 .full_snapshot_archives_dir
                 .path(),
+            None,
         )
         .unwrap();
 
@@ -1138,6 +1142,7 @@ fn test_incremental_snapshot_download_with_crossing_full_snapshot_interval_at_st
             validator_snapshot_test_config
                 .full_snapshot_archives_dir
                 .path(),
+            None,
         ) {
             if full_snapshot_slot >= validator_next_full_snapshot_slot {
                 if let Some(incremental_snapshot_slot) =
@@ -1146,6 +1151,7 @@ fn test_incremental_snapshot_download_with_crossing_full_snapshot_interval_at_st
                             .incremental_snapshot_archives_dir
                             .path(),
                         full_snapshot_slot,
+                        None,
                     )
                 {
                     if incremental_snapshot_slot >= validator_next_incremental_snapshot_slot {
@@ -1339,8 +1345,10 @@ fn test_snapshots_blockstore_floor() {
     trace!("Waiting for snapshot tar to be generated with slot",);
 
     let archive_info = loop {
-        let archive =
-            snapshot_utils::get_highest_full_snapshot_archive_info(full_snapshot_archives_dir);
+        let archive = snapshot_utils::get_highest_full_snapshot_archive_info(
+            full_snapshot_archives_dir,
+            None,
+        );
         if archive.is_some() {
             trace!("snapshot exists");
             break archive.unwrap();
diff --git a/merkle-tree/src/merkle_tree.rs b/merkle-tree/src/merkle_tree.rs
index d08e111d4e..2ee984ec46 100644
--- a/merkle-tree/src/merkle_tree.rs
+++ b/merkle-tree/src/merkle_tree.rs
@@ -18,7 +18,7 @@ macro_rules! hash_intermediate {
     }
 }
 
-#[derive(Debug)]
+#[derive(Default, Debug, Eq, Hash, PartialEq)]
 pub struct MerkleTree {
     leaf_count: usize,
     nodes: Vec<Hash>,
@@ -36,6 +36,14 @@ impl<'a> ProofEntry<'a> {
         assert!(left_sibling.is_none() ^ right_sibling.is_none());
         Self(target, left_sibling, right_sibling)
     }
+
+    pub fn get_left_sibling(&self) -> Option<&'a Hash> {
+        self.1
+    }
+
+    pub fn get_right_sibling(&self) -> Option<&'a Hash> {
+        self.2
+    }
 }
 
 #[derive(Debug, Default, PartialEq, Eq)]
@@ -60,6 +68,10 @@ impl<'a> Proof<'a> {
         });
         matches!(result, Some(_))
     }
+
+    pub fn get_proof_entries(self) -> Vec<ProofEntry<'a>> {
+        self.0
+    }
 }
 
 impl MerkleTree {
@@ -95,7 +107,7 @@ impl MerkleTree {
         }
     }
 
-    pub fn new<T: AsRef<[u8]>>(items: &[T]) -> Self {
+    pub fn new<T: AsRef<[u8]>>(items: &[T], sorted_hashes: bool) -> Self {
         let cap = MerkleTree::calculate_vec_capacity(items.len());
         let mut mt = MerkleTree {
             leaf_count: items.len(),
@@ -123,8 +135,20 @@ impl MerkleTree {
                     &mt.nodes[prev_level_start + prev_level_idx]
                 };
 
-                let hash = hash_intermediate!(lsib, rsib);
-                mt.nodes.push(hash);
+                // tip-distribution verification uses sorted hashing
+                if sorted_hashes {
+                    if lsib <= rsib {
+                        let hash = hash_intermediate!(lsib, rsib);
+                        mt.nodes.push(hash);
+                    } else {
+                        let hash = hash_intermediate!(rsib, lsib);
+                        mt.nodes.push(hash);
+                    }
+                } else {
+                    // hashing for solana internals
+                    let hash = hash_intermediate!(lsib, rsib);
+                    mt.nodes.push(hash);
+                }
             }
             prev_level_start = level_start;
             prev_level_len = level_len;
@@ -189,21 +213,21 @@ mod tests {
 
     #[test]
     fn test_tree_from_empty() {
-        let mt = MerkleTree::new::<[u8; 0]>(&[]);
+        let mt = MerkleTree::new::<[u8; 0]>(&[], false);
         assert_eq!(mt.get_root(), None);
     }
 
     #[test]
     fn test_tree_from_one() {
         let input = b"test";
-        let mt = MerkleTree::new(&[input]);
+        let mt = MerkleTree::new(&[input], false);
         let expected = hash_leaf!(input);
         assert_eq!(mt.get_root(), Some(&expected));
     }
 
     #[test]
     fn test_tree_from_many() {
-        let mt = MerkleTree::new(TEST);
+        let mt = MerkleTree::new(TEST, false);
         // This golden hash will need to be updated whenever the contents of `TEST` change in any
         // way, including addition, removal and reordering or any of the tree calculation algo
         // changes
@@ -215,7 +239,7 @@ mod tests {
 
     #[test]
     fn test_path_creation() {
-        let mt = MerkleTree::new(TEST);
+        let mt = MerkleTree::new(TEST, false);
         for (i, _s) in TEST.iter().enumerate() {
             let _path = mt.find_path(i).unwrap();
         }
@@ -223,13 +247,13 @@ mod tests {
 
     #[test]
     fn test_path_creation_bad_index() {
-        let mt = MerkleTree::new(TEST);
+        let mt = MerkleTree::new(TEST, false);
         assert_eq!(mt.find_path(TEST.len()), None);
     }
 
     #[test]
     fn test_path_verify_good() {
-        let mt = MerkleTree::new(TEST);
+        let mt = MerkleTree::new(TEST, false);
         for (i, s) in TEST.iter().enumerate() {
             let hash = hash_leaf!(s);
             let path = mt.find_path(i).unwrap();
@@ -239,7 +263,7 @@ mod tests {
 
     #[test]
     fn test_path_verify_bad() {
-        let mt = MerkleTree::new(TEST);
+        let mt = MerkleTree::new(TEST, false);
         for (i, s) in BAD.iter().enumerate() {
             let hash = hash_leaf!(s);
             let path = mt.find_path(i).unwrap();
diff --git a/multinode-demo/bootstrap-validator.sh b/multinode-demo/bootstrap-validator.sh
index f69c05d1ed..983b5eb6be 100755
--- a/multinode-demo/bootstrap-validator.sh
+++ b/multinode-demo/bootstrap-validator.sh
@@ -103,9 +103,39 @@ while [[ -n $1 ]]; do
     elif [[ $1 == --skip-require-tower ]]; then
       maybeRequireTower=false
       shift
+    elif [[ $1 == --relayer-url ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --block-engine-url ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --tip-payment-program-pubkey ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --tip-distribution-program-pubkey ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --commission-bps ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --shred-receiver-address ]]; then
+      args+=("$1" "$2")
+      shift 2
     elif [[ $1 = --log-messages-bytes-limit ]]; then
       args+=("$1" "$2")
       shift 2
+    elif [[ $1 == --geyser-plugin-config ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --trust-relayer-packets ]]; then
+      args+=("$1")
+      shift
+    elif [[ $1 == --rpc-threads ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --trust-block-engine-packets ]]; then
+      args+=("$1")
+      shift
     else
       echo "Unknown argument: $1"
       $program --help
@@ -141,6 +171,7 @@ args+=(
   --no-incremental-snapshots
   --identity "$identity"
   --vote-account "$vote_account"
+  --merkle-root-upload-authority "$identity"
   --rpc-faucet-address 127.0.0.1:9900
   --no-poh-speed-test
   --no-os-network-limits-test
@@ -150,6 +181,9 @@ args+=(
 )
 default_arg --gossip-port 8001
 default_arg --log -
+default_arg --tip-payment-program-pubkey "DThZmRNNXh7kvTQW9hXeGoWGPKktK8pgVAyoTLjH7UrT"
+default_arg --tip-distribution-program-pubkey "FjrdANjvo76aCYQ4kf9FM1R8aESUcEE6F8V7qyoVUQcM"
+default_arg --commission-bps 0
 
 
 pid=
diff --git a/multinode-demo/validator.sh b/multinode-demo/validator.sh
index 9090055b90..721008a661 100755
--- a/multinode-demo/validator.sh
+++ b/multinode-demo/validator.sh
@@ -85,6 +85,24 @@ while [[ -n $1 ]]; do
       vote_account=$2
       args+=("$1" "$2")
       shift 2
+    elif [[ $1 == --block-engine-url ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --relayer-url ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 = --merkle-root-upload-authority ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --tip-payment-program-pubkey ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --tip-distribution-program-pubkey ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --commission-bps ]]; then
+      args+=("$1" "$2")
+      shift 2
     elif [[ $1 = --init-complete-file ]]; then
       args+=("$1" "$2")
       shift 2
@@ -182,6 +200,24 @@ while [[ -n $1 ]]; do
     elif [[ $1 == --skip-require-tower ]]; then
       maybeRequireTower=false
       shift
+    elif [[ $1 == --rpc-pubsub-enable-block-subscription ]]; then
+      args+=("$1")
+      shift
+    elif [[ $1 == --geyser-plugin-config ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --trust-relayer-packets ]]; then
+      args+=("$1")
+      shift
+    elif [[ $1 == --rpc-threads ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --shred-receiver-address ]]; then
+      args+=("$1" "$2")
+      shift 2
+    elif [[ $1 == --trust-block-engine-packets ]]; then
+      args+=("$1")
+      shift
     elif [[ $1 = -h ]]; then
       usage "$@"
     else
@@ -256,6 +292,10 @@ fi
 
 default_arg --identity "$identity"
 default_arg --vote-account "$vote_account"
+default_arg --merkle-root-upload-authority "$identity"
+default_arg --tip-payment-program-pubkey "DThZmRNNXh7kvTQW9hXeGoWGPKktK8pgVAyoTLjH7UrT"
+default_arg --tip-distribution-program-pubkey "FjrdANjvo76aCYQ4kf9FM1R8aESUcEE6F8V7qyoVUQcM"
+default_arg --commission-bps 0
 default_arg --ledger "$ledger_dir"
 default_arg --log -
 default_arg --full-rpc-api
diff --git a/perf/src/sigverify.rs b/perf/src/sigverify.rs
index 47302b06c7..7cfacd33d3 100644
--- a/perf/src/sigverify.rs
+++ b/perf/src/sigverify.rs
@@ -110,7 +110,7 @@ pub fn init() {
 /// Returns true if the signatrue on the packet verifies.
 /// Caller must do packet.set_discard(true) if this returns false.
 #[must_use]
-fn verify_packet(packet: &mut Packet, reject_non_vote: bool) -> bool {
+pub fn verify_packet(packet: &mut Packet, reject_non_vote: bool) -> bool {
     // If this packet was already marked as discard, drop it
     if packet.meta().discard() {
         return false;
diff --git a/poh/src/poh_recorder.rs b/poh/src/poh_recorder.rs
index 342d3c8f2c..b8878c837f 100644
--- a/poh/src/poh_recorder.rs
+++ b/poh/src/poh_recorder.rs
@@ -60,9 +60,14 @@ pub enum PohRecorderError {
     SendError(#[from] SendError<WorkingBankEntry>),
 }
 
-type Result<T> = std::result::Result<T, PohRecorderError>;
+pub type Result<T> = std::result::Result<T, PohRecorderError>;
 
-pub type WorkingBankEntry = (Arc<Bank>, (Entry, u64));
+#[derive(Clone, Debug)]
+pub struct WorkingBankEntry {
+    pub bank: Arc<Bank>,
+    // normal entries have len == 1, bundles have len > 1
+    pub entries_ticks: Vec<(Entry, u64)>,
+}
 
 #[derive(Debug, Clone)]
 pub struct BankStart {
@@ -92,21 +97,19 @@ impl BankStart {
 type RecordResultSender = Sender<Result<Option<usize>>>;
 
 pub struct Record {
-    pub mixin: Hash,
-    pub transactions: Vec<VersionedTransaction>,
+    // non-bundles shall have mixins_txs.len() == 1, bundles shall have mixins_txs.len() > 1
+    pub mixins_txs: Vec<(Hash, Vec<VersionedTransaction>)>,
     pub slot: Slot,
     pub sender: RecordResultSender,
 }
 impl Record {
     pub fn new(
-        mixin: Hash,
-        transactions: Vec<VersionedTransaction>,
+        mixins_txs: Vec<(Hash, Vec<VersionedTransaction>)>,
         slot: Slot,
         sender: RecordResultSender,
     ) -> Self {
         Self {
-            mixin,
-            transactions,
+            mixins_txs,
             slot,
             sender,
         }
@@ -160,16 +163,21 @@ impl TransactionRecorder {
     pub fn record_transactions(
         &self,
         bank_slot: Slot,
-        transactions: Vec<VersionedTransaction>,
+        batches: Vec<Vec<VersionedTransaction>>,
     ) -> RecordTransactionsSummary {
         let mut record_transactions_timings = RecordTransactionsTimings::default();
         let mut starting_transaction_index = None;
 
-        if !transactions.is_empty() {
-            let (hash, hash_us) = measure_us!(hash_transactions(&transactions));
+        if !batches.is_empty() && !batches.iter().any(|b| b.is_empty()) {
+            let (hashes, hash_us) = measure_us!(batches
+                .iter()
+                .map(|b| hash_transactions(b))
+                .collect::<Vec<_>>());
             record_transactions_timings.hash_us = hash_us;
 
-            let (res, poh_record_us) = measure_us!(self.record(bank_slot, hash, transactions));
+            let hashes_transactions: Vec<_> = hashes.into_iter().zip(batches.into_iter()).collect();
+
+            let (res, poh_record_us) = measure_us!(self.record(bank_slot, hashes_transactions));
             record_transactions_timings.poh_record_us = poh_record_us;
 
             match res {
@@ -198,14 +206,13 @@ impl TransactionRecorder {
     pub fn record(
         &self,
         bank_slot: Slot,
-        mixin: Hash,
-        transactions: Vec<VersionedTransaction>,
+        mixins_txs: Vec<(Hash, Vec<VersionedTransaction>)>,
     ) -> Result<Option<usize>> {
         // create a new channel so that there is only 1 sender and when it goes out of scope, the receiver fails
         let (result_sender, result_receiver) = unbounded();
-        let res =
-            self.record_sender
-                .send(Record::new(mixin, transactions, bank_slot, result_sender));
+        let res = self
+            .record_sender
+            .send(Record::new(mixins_txs, bank_slot, result_sender));
         if res.is_err() {
             // If the channel is dropped, then the validator is shutting down so return that we are hitting
             //  the max tick height to stop transaction processing and flush any transactions in the pipeline.
@@ -658,7 +665,10 @@ impl PohRecorder {
 
             for tick in &self.tick_cache[..entry_count] {
                 working_bank.bank.register_tick(&tick.0.hash);
-                send_result = self.sender.send((working_bank.bank.clone(), tick.clone()));
+                send_result = self.sender.send(WorkingBankEntry {
+                    bank: working_bank.bank.clone(),
+                    entries_ticks: vec![tick.clone()],
+                });
                 if send_result.is_err() {
                     break;
                 }
@@ -838,16 +848,23 @@ impl PohRecorder {
     pub fn record(
         &mut self,
         bank_slot: Slot,
-        mixin: Hash,
-        transactions: Vec<VersionedTransaction>,
+        mixins_txs: &[(Hash, Vec<VersionedTransaction>)],
     ) -> Result<Option<usize>> {
         // Entries without transactions are used to track real-time passing in the ledger and
         // cannot be generated by `record()`
-        assert!(!transactions.is_empty(), "No transactions provided");
+        assert!(!mixins_txs.is_empty(), "No transactions provided");
+        assert!(
+            !mixins_txs.iter().any(|(_, txs)| txs.is_empty()),
+            "One of mixins is missing txs"
+        );
 
         let ((), report_metrics_time) = measure!(self.report_metrics(bank_slot), "report_metrics");
         self.report_metrics_us += report_metrics_time.as_us();
 
+        let mixins: Vec<Hash> = mixins_txs.iter().map(|(m, _)| *m).collect();
+        let transactions: Vec<Vec<VersionedTransaction>> =
+            mixins_txs.iter().map(|(_, tx)| tx.clone()).collect();
+
         loop {
             let (flush_cache_res, flush_cache_time) =
                 measure!(self.flush_cache(false), "flush_cache");
@@ -865,23 +882,36 @@ impl PohRecorder {
             let (mut poh_lock, poh_lock_time) = measure!(self.poh.lock().unwrap(), "poh_lock");
             self.record_lock_contention_us += poh_lock_time.as_us();
 
-            let (record_mixin_res, record_mixin_time) =
-                measure!(poh_lock.record(mixin), "record_mixin");
+            let (maybe_entries, record_mixin_time) =
+                measure!(poh_lock.record_bundle(&mixins), "record_mixin");
             self.record_us += record_mixin_time.as_us();
 
             drop(poh_lock);
 
-            if let Some(poh_entry) = record_mixin_res {
-                let num_transactions = transactions.len();
+            if let Some(entries) = maybe_entries {
+                assert_eq!(entries.len(), transactions.len());
+                let num_transactions = transactions.iter().map(|txs| txs.len()).sum();
                 let (send_entry_res, send_entry_time) = measure!(
                     {
-                        let entry = Entry {
-                            num_hashes: poh_entry.num_hashes,
-                            hash: poh_entry.hash,
-                            transactions,
-                        };
+                        let entries_tick_heights: Vec<(Entry, u64)> = entries
+                            .into_iter()
+                            .zip(transactions.into_iter())
+                            .map(|(poh_entry, transactions)| {
+                                (
+                                    Entry {
+                                        num_hashes: poh_entry.num_hashes,
+                                        hash: poh_entry.hash,
+                                        transactions,
+                                    },
+                                    self.tick_height,
+                                )
+                            })
+                            .collect();
                         let bank_clone = working_bank.bank.clone();
-                        self.sender.send((bank_clone, (entry, self.tick_height)))
+                        self.sender.send(WorkingBankEntry {
+                            bank: bank_clone,
+                            entries_ticks: entries_tick_heights,
+                        })
                     },
                     "send_poh_entry",
                 );
@@ -1258,13 +1288,17 @@ mod tests {
             assert_eq!(poh_recorder.tick_height, tick_height_before + 1);
             assert_eq!(poh_recorder.tick_cache.len(), 0);
             let mut num_entries = 0;
-            while let Ok((wbank, (_entry, _tick_height))) = entry_receiver.try_recv() {
+            while let Ok(WorkingBankEntry {
+                bank: wbank,
+                entries_ticks,
+            }) = entry_receiver.try_recv()
+            {
                 assert_eq!(wbank.slot(), bank1.slot());
-                num_entries += 1;
+                num_entries += entries_ticks.len();
             }
 
             // All the cached ticks, plus the new tick above should have been flushed
-            assert_eq!(num_entries, num_new_ticks + 1);
+            assert_eq!(num_entries as u64, num_new_ticks + 1);
         }
         Blockstore::destroy(&ledger_path).unwrap();
     }
@@ -1353,7 +1387,7 @@ mod tests {
             // We haven't yet reached the minimum tick height for the working bank,
             // so record should fail
             assert_matches!(
-                poh_recorder.record(bank1.slot(), h1, vec![tx.into()]),
+                poh_recorder.record(bank1.slot(), &[(h1, vec![tx.into()])]),
                 Err(PohRecorderError::MinHeightNotReached)
             );
             assert!(entry_receiver.try_recv().is_err());
@@ -1396,7 +1430,7 @@ mod tests {
             // However we hand over a bad slot so record fails
             let bad_slot = bank.slot() + 1;
             assert_matches!(
-                poh_recorder.record(bad_slot, h1, vec![tx.into()]),
+                poh_recorder.record(bad_slot, &[(h1, vec![tx.into()])]),
                 Err(PohRecorderError::MaxHeightReached)
             );
         }
@@ -1443,17 +1477,27 @@ mod tests {
             let tx = test_tx();
             let h1 = hash(b"hello world!");
             assert!(poh_recorder
-                .record(bank1.slot(), h1, vec![tx.into()])
+                .record(bank1.slot(), &[(h1, vec![tx.into()])])
                 .is_ok());
             assert_eq!(poh_recorder.tick_cache.len(), 0);
 
             //tick in the cache + entry
             for _ in 0..min_tick_height {
-                let (_bank, (e, _tick_height)) = entry_receiver.recv().unwrap();
+                let WorkingBankEntry {
+                    bank: _,
+                    entries_ticks,
+                } = entry_receiver.recv().unwrap();
+                assert_eq!(entries_ticks.len(), 1);
+                let e = entries_ticks.get(0).unwrap().0.clone();
                 assert!(e.is_tick());
             }
 
-            let (_bank, (e, _tick_height)) = entry_receiver.recv().unwrap();
+            let WorkingBankEntry {
+                bank: _,
+                entries_ticks,
+            } = entry_receiver.recv().unwrap();
+            assert_eq!(entries_ticks.len(), 1);
+            let e = entries_ticks.get(0).unwrap().0.clone();
             assert!(!e.is_tick());
         }
         Blockstore::destroy(&ledger_path).unwrap();
@@ -1489,10 +1533,16 @@ mod tests {
             let tx = test_tx();
             let h1 = hash(b"hello world!");
             assert!(poh_recorder
-                .record(bank.slot(), h1, vec![tx.into()])
+                .record(bank.slot(), &[(h1, vec![tx.into()])])
                 .is_err());
+
             for _ in 0..num_ticks_to_max {
-                let (_bank, (entry, _tick_height)) = entry_receiver.recv().unwrap();
+                let WorkingBankEntry {
+                    bank: _,
+                    entries_ticks,
+                } = entry_receiver.recv().unwrap();
+                assert_eq!(entries_ticks.len(), 1);
+                let entry = entries_ticks.get(0).unwrap().0.clone();
                 assert!(entry.is_tick());
             }
         }
@@ -1537,7 +1587,7 @@ mod tests {
             let tx1 = test_tx();
             let h1 = hash(b"hello world!");
             let record_result = poh_recorder
-                .record(bank.slot(), h1, vec![tx0.into(), tx1.into()])
+                .record(bank.slot(), &[(h1, vec![tx0.into(), tx1.into()])])
                 .unwrap()
                 .unwrap();
             assert_eq!(record_result, 0);
@@ -1554,7 +1604,7 @@ mod tests {
             let tx = test_tx();
             let h2 = hash(b"foobar");
             let record_result = poh_recorder
-                .record(bank.slot(), h2, vec![tx.into()])
+                .record(bank.slot(), &[(h2, vec![tx.into()])])
                 .unwrap()
                 .unwrap();
             assert_eq!(record_result, 2);
@@ -1821,7 +1871,7 @@ mod tests {
             let tx = test_tx();
             let h1 = hash(b"hello world!");
             assert!(poh_recorder
-                .record(bank.slot(), h1, vec![tx.into()])
+                .record(bank.slot(), &[(h1, vec![tx.into()])])
                 .is_err());
             assert!(poh_recorder.working_bank.is_none());
 
diff --git a/poh/src/poh_service.rs b/poh/src/poh_service.rs
index 4fcc918e5b..b86495e7cd 100644
--- a/poh/src/poh_service.rs
+++ b/poh/src/poh_service.rs
@@ -193,11 +193,12 @@ impl PohService {
         if let Ok(record) = record {
             if record
                 .sender
-                .send(poh_recorder.write().unwrap().record(
-                    record.slot,
-                    record.mixin,
-                    record.transactions,
-                ))
+                .send(
+                    poh_recorder
+                        .write()
+                        .unwrap()
+                        .record(record.slot, &record.mixins_txs),
+                )
                 .is_err()
             {
                 panic!("Error returning mixin hash");
@@ -256,11 +257,7 @@ impl PohService {
                 timing.total_lock_time_ns += lock_time.as_ns();
                 let mut record_time = Measure::start("record");
                 loop {
-                    let res = poh_recorder_l.record(
-                        record.slot,
-                        record.mixin,
-                        std::mem::take(&mut record.transactions),
-                    );
+                    let res = poh_recorder_l.record(record.slot, &record.mixins_txs);
                     // what do we do on failure here? Ignore for now.
                     let (_send_res, send_record_result_time) =
                         measure!(record.sender.send(res), "send_record_result");
@@ -382,6 +379,7 @@ impl PohService {
 mod tests {
     use {
         super::*,
+        crate::poh_recorder::WorkingBankEntry,
         rand::{thread_rng, Rng},
         solana_ledger::{
             blockstore::Blockstore,
@@ -461,11 +459,10 @@ mod tests {
                         loop {
                             // send some data
                             let mut time = Measure::start("record");
-                            let _ = poh_recorder.write().unwrap().record(
-                                bank_slot,
-                                h1,
-                                vec![tx.clone()],
-                            );
+                            let _ = poh_recorder
+                                .write()
+                                .unwrap()
+                                .record(bank_slot, &[(h1, vec![tx.clone()])]);
                             time.stop();
                             total_us += time.as_us();
                             total_times += 1;
@@ -510,7 +507,12 @@ mod tests {
 
             let time = Instant::now();
             while run_time != 0 || need_tick || need_entry || need_partial {
-                let (_bank, (entry, _tick_height)) = entry_receiver.recv().unwrap();
+                let WorkingBankEntry {
+                    bank: _,
+                    entries_ticks,
+                } = entry_receiver.recv().unwrap();
+                assert_eq!(entries_ticks.len(), 0);
+                let entry = entries_ticks.get(0).unwrap().0.clone();
 
                 if entry.is_tick() {
                     num_ticks += 1;
diff --git a/program-runtime/src/timings.rs b/program-runtime/src/timings.rs
index 0e2e4956a5..b63b20669f 100644
--- a/program-runtime/src/timings.rs
+++ b/program-runtime/src/timings.rs
@@ -8,7 +8,7 @@ use {
     },
 };
 
-#[derive(Default, Debug, PartialEq, Eq)]
+#[derive(Clone, Default, Debug, PartialEq, Eq)]
 pub struct ProgramTiming {
     pub accumulated_us: u64,
     pub accumulated_units: u64,
@@ -53,6 +53,7 @@ pub enum ExecuteTimingType {
     UpdateTransactionStatuses,
 }
 
+#[derive(Clone)]
 pub struct Metrics([u64; ExecuteTimingType::CARDINALITY]);
 
 impl Index<ExecuteTimingType> for Metrics {
@@ -316,7 +317,7 @@ impl ThreadExecuteTimings {
     }
 }
 
-#[derive(Debug, Default)]
+#[derive(Clone, Debug, Default)]
 pub struct ExecuteTimings {
     pub metrics: Metrics,
     pub details: ExecuteDetailsTimings,
@@ -340,9 +341,21 @@ impl ExecuteTimings {
             None => debug_assert!(idx < ExecuteTimingType::CARDINALITY, "Index out of bounds"),
         }
     }
+
+    pub fn accumulate_execute_units_and_time(&self) -> (u64, u64) {
+        self.details
+            .per_program_timings
+            .values()
+            .fold((0, 0), |(units, times), program_timings| {
+                (
+                    units.saturating_add(program_timings.accumulated_units),
+                    times.saturating_add(program_timings.accumulated_us),
+                )
+            })
+    }
 }
 
-#[derive(Default, Debug)]
+#[derive(Clone, Default, Debug)]
 pub struct ExecuteProcessInstructionTimings {
     pub total_us: u64,
     pub verify_caller_us: u64,
@@ -362,7 +375,7 @@ impl ExecuteProcessInstructionTimings {
     }
 }
 
-#[derive(Default, Debug)]
+#[derive(Clone, Default, Debug)]
 pub struct ExecuteAccessoryTimings {
     pub feature_set_clone_us: u64,
     pub compute_budget_process_transaction_us: u64,
@@ -387,7 +400,7 @@ impl ExecuteAccessoryTimings {
     }
 }
 
-#[derive(Default, Debug, PartialEq, Eq)]
+#[derive(Clone, Default, Debug, PartialEq, Eq)]
 pub struct ExecuteDetailsTimings {
     pub serialize_us: u64,
     pub create_vm_us: u64,
diff --git a/program-test/src/programs.rs b/program-test/src/programs.rs
index ed96be7644..c01b5f4a82 100644
--- a/program-test/src/programs.rs
+++ b/program-test/src/programs.rs
@@ -21,6 +21,13 @@ mod spl_associated_token_account {
     solana_sdk::declare_id!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
 }
 
+mod jito_tip_payment {
+    solana_sdk::declare_id!("T1pyyaTNZsKv2WcRAB8oVnk93mLJw2XzjtVYqCsaHqt");
+}
+mod jito_tip_distribution {
+    solana_sdk::declare_id!("4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7");
+}
+
 static SPL_PROGRAMS: &[(Pubkey, Pubkey, &[u8])] = &[
     (
         spl_token::ID,
@@ -47,6 +54,16 @@ static SPL_PROGRAMS: &[(Pubkey, Pubkey, &[u8])] = &[
         solana_sdk::bpf_loader::ID,
         include_bytes!("programs/spl_associated_token_account-1.1.1.so"),
     ),
+    (
+        jito_tip_distribution::ID,
+        solana_sdk::bpf_loader::ID,
+        include_bytes!("programs/jito_tip_distribution-0.1.3.so"),
+    ),
+    (
+        jito_tip_payment::ID,
+        solana_sdk::bpf_loader::ID,
+        include_bytes!("programs/jito_tip_payment-0.1.3.so"),
+    ),
 ];
 
 pub fn spl_programs(rent: &Rent) -> Vec<(Pubkey, AccountSharedData)> {
diff --git a/program-test/src/programs/jito_tip_distribution-0.1.3.so b/program-test/src/programs/jito_tip_distribution-0.1.3.so
new file mode 100644
index 0000000000000000000000000000000000000000..094ad26f310bb20cad7985fb8416c8004593e2aa
GIT binary patch
literal 439968
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
H)A;`%XKV4{

literal 0
HcmV?d00001

diff --git a/program-test/src/programs/jito_tip_payment-0.1.3.so b/program-test/src/programs/jito_tip_payment-0.1.3.so
new file mode 100644
index 0000000000000000000000000000000000000000..4b747aa3cc6e120680200e9dcabc6ad56d6e4279
GIT binary patch
literal 433224
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
WsOP>rXmun1Y<0%|<~OU`-2Z=OoC{X~

literal 0
HcmV?d00001

diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock
index 488294c68f..b4b9565aa2 100644
--- a/programs/sbf/Cargo.lock
+++ b/programs/sbf/Cargo.lock
@@ -116,6 +116,145 @@ dependencies = [
  "alloc-no-stdlib",
 ]
 
+[[package]]
+name = "anchor-attribute-access-control"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "regex",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-account"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "bs58 0.4.0",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "rustversion",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-constant"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "proc-macro2 1.0.60",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-error"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-event"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-interface"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "heck 0.3.3",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-program"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-attribute-state"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-derive-accounts"
+version = "0.24.2"
+dependencies = [
+ "anchor-syn",
+ "anyhow",
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "anchor-lang"
+version = "0.24.2"
+dependencies = [
+ "anchor-attribute-access-control",
+ "anchor-attribute-account",
+ "anchor-attribute-constant",
+ "anchor-attribute-error",
+ "anchor-attribute-event",
+ "anchor-attribute-interface",
+ "anchor-attribute-program",
+ "anchor-attribute-state",
+ "anchor-derive-accounts",
+ "arrayref",
+ "base64 0.13.1",
+ "bincode",
+ "borsh 0.10.3",
+ "bytemuck",
+ "solana-program",
+ "thiserror",
+]
+
+[[package]]
+name = "anchor-syn"
+version = "0.24.2"
+dependencies = [
+ "anyhow",
+ "bs58 0.3.1",
+ "heck 0.3.3",
+ "proc-macro2 1.0.60",
+ "proc-macro2-diagnostics",
+ "quote 1.0.28",
+ "serde",
+ "serde_json",
+ "sha2 0.9.9",
+ "syn 1.0.109",
+ "thiserror",
+]
+
 [[package]]
 name = "android_system_properties"
 version = "0.1.4"
@@ -277,9 +416,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
 
 [[package]]
 name = "arrayvec"
-version = "0.7.1"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
 
 [[package]]
 name = "ascii"
@@ -555,9 +694,9 @@ dependencies = [
 
 [[package]]
 name = "blake3"
-version = "1.3.3"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef"
+checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87"
 dependencies = [
  "arrayref",
  "arrayvec",
@@ -630,7 +769,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b"
 dependencies = [
  "borsh-derive 0.10.3",
- "hashbrown 0.11.2",
+ "hashbrown 0.13.2",
 ]
 
 [[package]]
@@ -724,6 +863,12 @@ dependencies = [
  "alloc-stdlib",
 ]
 
+[[package]]
+name = "bs58"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb"
+
 [[package]]
 name = "bs58"
 version = "0.4.0"
@@ -999,9 +1144,9 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3"
 
 [[package]]
 name = "constant_time_eq"
-version = "0.2.5"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b"
+checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
 
 [[package]]
 name = "convert_case"
@@ -1199,6 +1344,17 @@ version = "2.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
 
+[[package]]
+name = "default-env"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f753eb82d29277e79efc625e84aecacfd4851ee50e05a8573a4740239a77bfd3"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
+]
+
 [[package]]
 name = "der"
 version = "0.5.1"
@@ -2248,6 +2404,49 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
 
+[[package]]
+name = "jito-programs-vote-state"
+version = "0.1.4"
+dependencies = [
+ "anchor-lang",
+ "bincode",
+ "serde",
+ "serde_derive",
+ "solana-program",
+]
+
+[[package]]
+name = "jito-protos"
+version = "1.16.20"
+dependencies = [
+ "bytes",
+ "prost 0.11.9",
+ "prost-types 0.11.9",
+ "protobuf-src",
+ "tonic 0.8.3",
+ "tonic-build 0.8.4",
+]
+
+[[package]]
+name = "jito-tip-distribution"
+version = "0.1.4"
+dependencies = [
+ "anchor-lang",
+ "default-env",
+ "jito-programs-vote-state",
+ "solana-program",
+ "solana-security-txt",
+]
+
+[[package]]
+name = "jito-tip-payment"
+version = "0.1.4"
+dependencies = [
+ "anchor-lang",
+ "default-env",
+ "solana-security-txt",
+]
+
 [[package]]
 name = "jobserver"
 version = "0.1.21"
@@ -3478,6 +3677,19 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada"
+dependencies = [
+ "proc-macro2 1.0.60",
+ "quote 1.0.28",
+ "syn 1.0.109",
+ "version_check",
+ "yansi",
+]
+
 [[package]]
 name = "prost"
 version = "0.9.0"
@@ -4446,7 +4658,7 @@ dependencies = [
  "Inflector",
  "base64 0.21.2",
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "bv",
  "lazy_static",
  "serde",
@@ -4514,12 +4726,14 @@ dependencies = [
  "futures 0.3.28",
  "solana-banks-interface",
  "solana-client",
+ "solana-gossip",
  "solana-runtime",
  "solana-sdk",
  "solana-send-transaction-service",
  "tarpc",
  "tokio",
  "tokio-serde",
+ "tokio-stream",
 ]
 
 [[package]]
@@ -4581,6 +4795,25 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "solana-bundle"
+version = "1.16.20"
+dependencies = [
+ "anchor-lang",
+ "itertools",
+ "log",
+ "serde",
+ "solana-ledger",
+ "solana-logger",
+ "solana-measure",
+ "solana-poh",
+ "solana-program-runtime",
+ "solana-runtime",
+ "solana-sdk",
+ "solana-transaction-status",
+ "thiserror",
+]
+
 [[package]]
 name = "solana-clap-utils"
 version = "1.16.20"
@@ -4710,9 +4943,10 @@ dependencies = [
 name = "solana-core"
 version = "1.16.20"
 dependencies = [
+ "anchor-lang",
  "base64 0.21.2",
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "chrono",
  "crossbeam-channel",
  "dashmap",
@@ -4720,11 +4954,16 @@ dependencies = [
  "etcd-client",
  "histogram",
  "itertools",
+ "jito-protos",
+ "jito-tip-distribution",
+ "jito-tip-payment",
  "lazy_static",
  "log",
  "lru",
  "min-max-heap",
  "num_enum 0.6.1",
+ "prost 0.11.9",
+ "prost-types 0.11.9",
  "rand 0.7.3",
  "rand_chacha 0.2.2",
  "rayon",
@@ -4734,6 +4973,7 @@ dependencies = [
  "serde_derive",
  "solana-address-lookup-table-program",
  "solana-bloom",
+ "solana-bundle",
  "solana-client",
  "solana-entry",
  "solana-frozen-abi",
@@ -4751,6 +4991,7 @@ dependencies = [
  "solana-rpc",
  "solana-rpc-client-api",
  "solana-runtime",
+ "solana-runtime-plugin",
  "solana-sdk",
  "solana-send-transaction-service",
  "solana-streamer",
@@ -4765,6 +5006,8 @@ dependencies = [
  "tempfile",
  "thiserror",
  "tokio",
+ "tonic 0.8.3",
+ "tonic-build 0.8.4",
  "trees",
 ]
 
@@ -4830,7 +5073,7 @@ dependencies = [
  "ahash 0.8.3",
  "blake3",
  "block-buffer 0.10.4",
- "bs58",
+ "bs58 0.4.0",
  "bv",
  "byteorder 1.4.3",
  "cc",
@@ -4889,7 +5132,7 @@ dependencies = [
 name = "solana-geyser-plugin-manager"
 version = "1.16.20"
 dependencies = [
- "bs58",
+ "bs58 0.4.0",
  "crossbeam-channel",
  "json5",
  "jsonrpc-core",
@@ -5140,7 +5383,7 @@ dependencies = [
  "blake3",
  "borsh 0.10.3",
  "borsh 0.9.3",
- "bs58",
+ "bs58 0.4.0",
  "bv",
  "bytemuck",
  "cc",
@@ -5309,7 +5552,7 @@ version = "1.16.20"
 dependencies = [
  "base64 0.21.2",
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "crossbeam-channel",
  "dashmap",
  "itertools",
@@ -5327,6 +5570,7 @@ dependencies = [
  "serde_json",
  "soketto",
  "solana-account-decoder",
+ "solana-bundle",
  "solana-client",
  "solana-entry",
  "solana-faucet",
@@ -5336,6 +5580,7 @@ dependencies = [
  "solana-metrics",
  "solana-perf",
  "solana-poh",
+ "solana-program-runtime",
  "solana-rayon-threadlimit",
  "solana-rpc-client-api",
  "solana-runtime",
@@ -5363,7 +5608,7 @@ dependencies = [
  "async-trait",
  "base64 0.21.2",
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "indicatif",
  "log",
  "reqwest",
@@ -5385,7 +5630,7 @@ name = "solana-rpc-client-api"
 version = "1.16.20"
 dependencies = [
  "base64 0.21.2",
- "bs58",
+ "bs58 0.4.0",
  "jsonrpc-core",
  "reqwest",
  "semver",
@@ -5393,6 +5638,8 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "solana-account-decoder",
+ "solana-bundle",
+ "solana-runtime",
  "solana-sdk",
  "solana-transaction-status",
  "solana-version",
@@ -5478,6 +5725,24 @@ dependencies = [
  "zstd",
 ]
 
+[[package]]
+name = "solana-runtime-plugin"
+version = "1.16.20"
+dependencies = [
+ "crossbeam-channel",
+ "json5",
+ "jsonrpc-core",
+ "jsonrpc-core-client",
+ "jsonrpc-derive",
+ "jsonrpc-ipc-server",
+ "jsonrpc-server-utils",
+ "libloading",
+ "log",
+ "solana-runtime",
+ "solana-sdk",
+ "thiserror",
+]
+
 [[package]]
 name = "solana-sbf-programs"
 version = "1.16.20"
@@ -5876,12 +6141,13 @@ dependencies = [
 name = "solana-sdk"
 version = "1.16.20"
 dependencies = [
+ "anchor-lang",
  "assert_matches",
  "base64 0.21.2",
  "bincode",
  "bitflags",
  "borsh 0.10.3",
- "bs58",
+ "bs58 0.4.0",
  "bytemuck",
  "byteorder 1.4.3",
  "chrono",
@@ -5927,13 +6193,19 @@ dependencies = [
 name = "solana-sdk-macro"
 version = "1.16.20"
 dependencies = [
- "bs58",
+ "bs58 0.4.0",
  "proc-macro2 1.0.60",
  "quote 1.0.28",
  "rustversion",
  "syn 2.0.18",
 ]
 
+[[package]]
+name = "solana-security-txt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183"
+
 [[package]]
 name = "solana-send-transaction-service"
 version = "1.16.20"
@@ -5941,6 +6213,7 @@ dependencies = [
  "crossbeam-channel",
  "log",
  "solana-client",
+ "solana-gossip",
  "solana-measure",
  "solana-metrics",
  "solana-runtime",
@@ -5998,7 +6271,7 @@ name = "solana-storage-proto"
 version = "1.16.20"
 dependencies = [
  "bincode",
- "bs58",
+ "bs58 0.4.0",
  "prost 0.11.9",
  "protobuf-src",
  "serde",
@@ -6124,7 +6397,7 @@ dependencies = [
  "base64 0.21.2",
  "bincode",
  "borsh 0.10.3",
- "bs58",
+ "bs58 0.4.0",
  "lazy_static",
  "log",
  "serde",
@@ -6201,6 +6474,7 @@ dependencies = [
  "solana-rpc-client",
  "solana-rpc-client-api",
  "solana-runtime",
+ "solana-runtime-plugin",
  "solana-sdk",
  "solana-send-transaction-service",
  "solana-storage-bigtable",
@@ -6212,6 +6486,7 @@ dependencies = [
  "symlink",
  "thiserror",
  "tikv-jemallocator",
+ "tonic 0.8.3",
 ]
 
 [[package]]
@@ -7062,6 +7337,7 @@ dependencies = [
  "pin-project",
  "prost 0.11.9",
  "prost-derive 0.11.9",
+ "rustls-native-certs",
  "rustls-pemfile 1.0.0",
  "tokio",
  "tokio-rustls 0.23.2",
@@ -7072,6 +7348,7 @@ dependencies = [
  "tower-service",
  "tracing",
  "tracing-futures",
+ "webpki-roots",
 ]
 
 [[package]]
@@ -7837,6 +8114,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "yansi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+
 [[package]]
 name = "yasna"
 version = "0.5.0"
diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs
index 9a3910f4f7..4f09daaf88 100644
--- a/programs/sbf/tests/programs.rs
+++ b/programs/sbf/tests/programs.rs
@@ -135,7 +135,7 @@ fn execute_transactions(
     let batch = bank.prepare_batch_for_tests(txs.clone());
     let mut timings = ExecuteTimings::default();
     let mut mint_decimals = HashMap::new();
-    let tx_pre_token_balances = collect_token_balances(&bank, &batch, &mut mint_decimals);
+    let tx_pre_token_balances = collect_token_balances(&bank, &batch, &mut mint_decimals, None);
     let (
         TransactionResults {
             execution_results, ..
@@ -155,7 +155,7 @@ fn execute_transactions(
         &mut timings,
         None,
     );
-    let tx_post_token_balances = collect_token_balances(&bank, &batch, &mut mint_decimals);
+    let tx_post_token_balances = collect_token_balances(&bank, &batch, &mut mint_decimals, None);
 
     izip!(
         txs.iter(),
diff --git a/rpc-client-api/Cargo.toml b/rpc-client-api/Cargo.toml
index 92bc9d4958..a7d3bae207 100644
--- a/rpc-client-api/Cargo.toml
+++ b/rpc-client-api/Cargo.toml
@@ -19,6 +19,8 @@ serde = { workspace = true }
 serde_derive = { workspace = true }
 serde_json = { workspace = true }
 solana-account-decoder = { workspace = true }
+solana-bundle = { workspace = true }
+solana-runtime = { workspace = true }
 solana-sdk = { workspace = true }
 solana-transaction-status = { workspace = true }
 solana-version = { workspace = true }
diff --git a/rpc-client-api/src/bundles.rs b/rpc-client-api/src/bundles.rs
new file mode 100644
index 0000000000..f0a6d99933
--- /dev/null
+++ b/rpc-client-api/src/bundles.rs
@@ -0,0 +1,166 @@
+//! Use a separate file for Jito related code to minimize upstream merge conflicts.
+
+use {
+    crate::config::RpcSimulateTransactionAccountsConfig,
+    solana_account_decoder::UiAccount,
+    solana_bundle::{bundle_execution::LoadAndExecuteBundleError, BundleExecutionError},
+    solana_runtime::bank::TransactionExecutionResult,
+    solana_sdk::{
+        clock::Slot,
+        commitment_config::{CommitmentConfig, CommitmentLevel},
+        signature::Signature,
+        transaction::TransactionError,
+    },
+    solana_transaction_status::{UiTransactionEncoding, UiTransactionReturnData},
+    thiserror::Error,
+};
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(rename_all = "camelCase")]
+pub enum RpcBundleSimulationSummary {
+    /// error and offending transaction signature if applicable
+    Failed {
+        error: RpcBundleExecutionError,
+        tx_signature: Option<String>,
+    },
+    Succeeded,
+}
+
+#[derive(Error, Debug, Clone, Serialize, Deserialize)]
+pub enum RpcBundleExecutionError {
+    #[error("The bank has hit the max allotted time for processing transactions")]
+    BankProcessingTimeLimitReached,
+
+    #[error("Error locking bundle because a transaction is malformed")]
+    BundleLockError,
+
+    #[error("Bundle execution timed out")]
+    BundleExecutionTimeout,
+
+    #[error("The bundle exceeds the cost model")]
+    ExceedsCostModel,
+
+    #[error("Invalid pre or post accounts")]
+    InvalidPreOrPostAccounts,
+
+    #[error("PoH record error: {0}")]
+    PohRecordError(String),
+
+    #[error("Tip payment error: {0}")]
+    TipError(String),
+
+    #[error("A transaction in the bundle failed to execute: [signature={0}, error={1}]")]
+    TransactionFailure(Signature, String),
+}
+
+impl From<BundleExecutionError> for RpcBundleExecutionError {
+    fn from(bundle_execution_error: BundleExecutionError) -> Self {
+        match bundle_execution_error {
+            BundleExecutionError::BankProcessingTimeLimitReached => {
+                Self::BankProcessingTimeLimitReached
+            }
+            BundleExecutionError::ExceedsCostModel => Self::ExceedsCostModel,
+            BundleExecutionError::TransactionFailure(load_and_execute_bundle_error) => {
+                match load_and_execute_bundle_error {
+                    LoadAndExecuteBundleError::ProcessingTimeExceeded(_) => {
+                        Self::BundleExecutionTimeout
+                    }
+                    LoadAndExecuteBundleError::LockError {
+                        signature,
+                        transaction_error,
+                    } => Self::TransactionFailure(signature, transaction_error.to_string()),
+                    LoadAndExecuteBundleError::TransactionError {
+                        signature,
+                        execution_result,
+                    } => match *execution_result {
+                        TransactionExecutionResult::Executed { details, .. } => {
+                            let err_msg = if let Err(e) = details.status {
+                                e.to_string()
+                            } else {
+                                "Unknown error".to_string()
+                            };
+                            Self::TransactionFailure(signature, err_msg)
+                        }
+                        TransactionExecutionResult::NotExecuted(e) => {
+                            Self::TransactionFailure(signature, e.to_string())
+                        }
+                    },
+                    LoadAndExecuteBundleError::InvalidPreOrPostAccounts => {
+                        Self::InvalidPreOrPostAccounts
+                    }
+                }
+            }
+            BundleExecutionError::LockError => Self::BundleLockError,
+            BundleExecutionError::PohRecordError(e) => Self::PohRecordError(e.to_string()),
+            BundleExecutionError::TipError(e) => Self::TipError(e.to_string()),
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct RpcSimulateBundleResult {
+    pub summary: RpcBundleSimulationSummary,
+    pub transaction_results: Vec<RpcSimulateBundleTransactionResult>,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct RpcSimulateBundleTransactionResult {
+    pub err: Option<TransactionError>,
+    pub logs: Option<Vec<String>>,
+    pub pre_execution_accounts: Option<Vec<UiAccount>>,
+    pub post_execution_accounts: Option<Vec<UiAccount>>,
+    pub units_consumed: Option<u64>,
+    pub return_data: Option<UiTransactionReturnData>,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RpcSimulateBundleConfig {
+    /// Gives the state of accounts pre/post transaction execution.
+    /// The length of each of these must be equal to the number transactions.   
+    pub pre_execution_accounts_configs: Vec<Option<RpcSimulateTransactionAccountsConfig>>,
+    pub post_execution_accounts_configs: Vec<Option<RpcSimulateTransactionAccountsConfig>>,
+
+    /// Specifies the encoding scheme of the contained transactions.
+    pub transaction_encoding: Option<UiTransactionEncoding>,
+
+    /// Specifies the bank to run simulation against.
+    pub simulation_bank: Option<SimulationSlotConfig>,
+
+    /// Opt to skip sig-verify for faster performance.
+    #[serde(default)]
+    pub skip_sig_verify: bool,
+
+    /// Replace recent blockhash to simulate old transactions without resigning.
+    #[serde(default)]
+    pub replace_recent_blockhash: bool,
+}
+
+#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
+#[serde(rename_all = "camelCase")]
+pub enum SimulationSlotConfig {
+    /// Simulate on top of bank with the provided commitment.
+    Commitment(CommitmentConfig),
+
+    /// Simulate on the provided slot's bank.
+    Slot(Slot),
+
+    /// Simulates on top of the RPC's highest slot's bank i.e. the working bank.
+    Tip,
+}
+
+impl Default for SimulationSlotConfig {
+    fn default() -> Self {
+        Self::Commitment(CommitmentConfig {
+            commitment: CommitmentLevel::Confirmed,
+        })
+    }
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RpcBundleRequest {
+    pub encoded_transactions: Vec<String>,
+}
diff --git a/rpc-client-api/src/config.rs b/rpc-client-api/src/config.rs
index 9ecff334ca..d16df2d165 100644
--- a/rpc-client-api/src/config.rs
+++ b/rpc-client-api/src/config.rs
@@ -46,7 +46,7 @@ pub struct RpcSimulateTransactionConfig {
     pub min_context_slot: Option<Slot>,
 }
 
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct RpcRequestAirdropConfig {
     pub recent_blockhash: Option<String>, // base-58 encoded blockhash
diff --git a/rpc-client-api/src/lib.rs b/rpc-client-api/src/lib.rs
index 9be15cbab4..1d8b1e2dd1 100644
--- a/rpc-client-api/src/lib.rs
+++ b/rpc-client-api/src/lib.rs
@@ -1,5 +1,6 @@
 #![allow(clippy::integer_arithmetic)]
 
+pub mod bundles;
 pub mod client_error;
 pub mod config;
 pub mod custom_error;
diff --git a/rpc-client-api/src/request.rs b/rpc-client-api/src/request.rs
index e9c6ef9b38..e1e5cfd756 100644
--- a/rpc-client-api/src/request.rs
+++ b/rpc-client-api/src/request.rs
@@ -113,6 +113,7 @@ pub enum RpcRequest {
     RequestAirdrop,
     SendTransaction,
     SimulateTransaction,
+    SimulateBundle,
     SignVote,
 }
 
@@ -189,6 +190,7 @@ impl fmt::Display for RpcRequest {
             RpcRequest::RequestAirdrop => "requestAirdrop",
             RpcRequest::SendTransaction => "sendTransaction",
             RpcRequest::SimulateTransaction => "simulateTransaction",
+            RpcRequest::SimulateBundle => "simulateBundle",
             RpcRequest::SignVote => "signVote",
         };
 
@@ -258,6 +260,7 @@ pub enum RpcError {
     RpcRequestError(String),
     #[error("RPC response error {code}: {message} {data}")]
     RpcResponseError {
+        request_id: u64,
         code: i64,
         message: String,
         data: RpcResponseErrorData,
diff --git a/rpc-client-api/src/response.rs b/rpc-client-api/src/response.rs
index 7591c58c03..e20b92d1fc 100644
--- a/rpc-client-api/src/response.rs
+++ b/rpc-client-api/src/response.rs
@@ -36,6 +36,7 @@ impl<T> OptionalContext<T> {
     }
 }
 
+pub type BatchRpcResult<T> = client_error::Result<Vec<BatchResponse<T>>>;
 pub type RpcResult<T> = client_error::Result<Response<T>>;
 
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -46,6 +47,15 @@ pub struct RpcResponseContext {
     pub api_version: Option<RpcApiVersion>,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BatchRpcResponseContext {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub slot: Option<Slot>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub api_version: Option<RpcApiVersion>,
+}
+
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct RpcApiVersion(semver::Version);
 
@@ -92,6 +102,12 @@ impl RpcResponseContext {
     }
 }
 
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct BatchResponse<T> {
+    pub id: u64,
+    pub result: Response<T>,
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Response<T> {
     pub context: RpcResponseContext,
diff --git a/rpc-client/src/http_sender.rs b/rpc-client/src/http_sender.rs
index 902f86ce63..dc278db38d 100644
--- a/rpc-client/src/http_sender.rs
+++ b/rpc-client/src/http_sender.rs
@@ -11,7 +11,7 @@ use {
     },
     solana_rpc_client_api::{
         client_error::Result,
-        custom_error,
+        custom_error::{self},
         error_object::RpcErrorObject,
         request::{RpcError, RpcRequest, RpcResponseErrorData},
         response::RpcSimulateTransactionResult,
@@ -72,62 +72,74 @@ impl HttpSender {
             stats: RwLock::new(RpcTransportStats::default()),
         }
     }
-}
 
-struct StatsUpdater<'a> {
-    stats: &'a RwLock<RpcTransportStats>,
-    request_start_time: Instant,
-    rate_limited_time: Duration,
-}
+    fn check_response(json: &serde_json::Value) -> Result<()> {
+        if json["error"].is_object() {
+            return match serde_json::from_value::<RpcErrorObject>(json["error"].clone()) {
+                Ok(rpc_error_object) => {
+                    let data = match rpc_error_object.code {
+                        custom_error::JSON_RPC_SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE => {
+                            match serde_json::from_value::<RpcSimulateTransactionResult>(
+                                json["error"]["data"].clone(),
+                            ) {
+                                Ok(data) => {
+                                    RpcResponseErrorData::SendTransactionPreflightFailure(data)
+                                }
+                                Err(err) => {
+                                    debug!(
+                                        "Failed to deserialize RpcSimulateTransactionResult: {:?}",
+                                        err
+                                    );
+                                    RpcResponseErrorData::Empty
+                                }
+                            }
+                        }
+                        custom_error::JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY => {
+                            match serde_json::from_value::<custom_error::NodeUnhealthyErrorData>(
+                                json["error"]["data"].clone(),
+                            ) {
+                                Ok(custom_error::NodeUnhealthyErrorData { num_slots_behind }) => {
+                                    RpcResponseErrorData::NodeUnhealthy { num_slots_behind }
+                                }
+                                Err(_err) => RpcResponseErrorData::Empty,
+                            }
+                        }
+                        _ => RpcResponseErrorData::Empty,
+                    };
 
-impl<'a> StatsUpdater<'a> {
-    fn new(stats: &'a RwLock<RpcTransportStats>) -> Self {
-        Self {
-            stats,
-            request_start_time: Instant::now(),
-            rate_limited_time: Duration::default(),
+                    Err(RpcError::RpcResponseError {
+                        request_id: json["id"].as_u64().unwrap(),
+                        code: rpc_error_object.code,
+                        message: rpc_error_object.message,
+                        data,
+                    }
+                    .into())
+                }
+                Err(err) => Err(RpcError::RpcRequestError(format!(
+                    "Failed to deserialize RPC error response: {} [{}]",
+                    serde_json::to_string(&json["error"]).unwrap(),
+                    err
+                ))
+                .into()),
+            };
         }
+        Ok(())
     }
 
-    fn add_rate_limited_time(&mut self, duration: Duration) {
-        self.rate_limited_time += duration;
-    }
-}
-
-impl<'a> Drop for StatsUpdater<'a> {
-    fn drop(&mut self) {
-        let mut stats = self.stats.write().unwrap();
-        stats.request_count += 1;
-        stats.elapsed_time += Instant::now().duration_since(self.request_start_time);
-        stats.rate_limited_time += self.rate_limited_time;
-    }
-}
-
-#[async_trait]
-impl RpcSender for HttpSender {
-    fn get_transport_stats(&self) -> RpcTransportStats {
-        self.stats.read().unwrap().clone()
-    }
-
-    async fn send(
+    async fn do_send_with_retry(
         &self,
-        request: RpcRequest,
-        params: serde_json::Value,
-    ) -> Result<serde_json::Value> {
+        request: serde_json::Value,
+    ) -> reqwest::Result<serde_json::Value> {
         let mut stats_updater = StatsUpdater::new(&self.stats);
-
-        let request_id = self.request_id.fetch_add(1, Ordering::Relaxed);
-        let request_json = request.build_request_json(request_id, params).to_string();
-
         let mut too_many_requests_retries = 5;
         loop {
             let response = {
                 let client = self.client.clone();
-                let request_json = request_json.clone();
+                let request = request.to_string();
                 client
                     .post(&self.url)
                     .header(CONTENT_TYPE, "application/json")
-                    .body(request_json)
+                    .body(request)
                     .send()
                     .await
             }?;
@@ -155,54 +167,81 @@ impl RpcSender for HttpSender {
 
                     sleep(duration).await;
                     stats_updater.add_rate_limited_time(duration);
+
                     continue;
                 }
-                return Err(response.error_for_status().unwrap_err().into());
-            }
 
-            let mut json = response.json::<serde_json::Value>().await?;
-            if json["error"].is_object() {
-                return match serde_json::from_value::<RpcErrorObject>(json["error"].clone()) {
-                    Ok(rpc_error_object) => {
-                        let data = match rpc_error_object.code {
-                                    custom_error::JSON_RPC_SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE => {
-                                        match serde_json::from_value::<RpcSimulateTransactionResult>(json["error"]["data"].clone()) {
-                                            Ok(data) => RpcResponseErrorData::SendTransactionPreflightFailure(data),
-                                            Err(err) => {
-                                                debug!("Failed to deserialize RpcSimulateTransactionResult: {:?}", err);
-                                                RpcResponseErrorData::Empty
-                                            }
-                                        }
-                                    },
-                                    custom_error::JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY => {
-                                        match serde_json::from_value::<custom_error::NodeUnhealthyErrorData>(json["error"]["data"].clone()) {
-                                            Ok(custom_error::NodeUnhealthyErrorData {num_slots_behind}) => RpcResponseErrorData::NodeUnhealthy {num_slots_behind},
-                                            Err(_err) => {
-                                                RpcResponseErrorData::Empty
-                                            }
-                                        }
-                                    },
-                                    _ => RpcResponseErrorData::Empty
-                                };
-
-                        Err(RpcError::RpcResponseError {
-                            code: rpc_error_object.code,
-                            message: rpc_error_object.message,
-                            data,
-                        }
-                        .into())
-                    }
-                    Err(err) => Err(RpcError::RpcRequestError(format!(
-                        "Failed to deserialize RPC error response: {} [{}]",
-                        serde_json::to_string(&json["error"]).unwrap(),
-                        err
-                    ))
-                    .into()),
-                };
+                return Err(response.error_for_status().unwrap_err());
             }
-            return Ok(json["result"].take());
+
+            return response.json::<serde_json::Value>().await;
         }
     }
+}
+
+struct StatsUpdater<'a> {
+    stats: &'a RwLock<RpcTransportStats>,
+    request_start_time: Instant,
+    rate_limited_time: Duration,
+}
+
+impl<'a> StatsUpdater<'a> {
+    fn new(stats: &'a RwLock<RpcTransportStats>) -> Self {
+        Self {
+            stats,
+            request_start_time: Instant::now(),
+            rate_limited_time: Duration::default(),
+        }
+    }
+
+    fn add_rate_limited_time(&mut self, duration: Duration) {
+        self.rate_limited_time += duration;
+    }
+}
+
+impl<'a> Drop for StatsUpdater<'a> {
+    fn drop(&mut self) {
+        let mut stats = self.stats.write().unwrap();
+        stats.request_count += 1;
+        stats.elapsed_time += Instant::now().duration_since(self.request_start_time);
+        stats.rate_limited_time += self.rate_limited_time;
+    }
+}
+
+#[async_trait]
+impl RpcSender for HttpSender {
+    async fn send(
+        &self,
+        request: RpcRequest,
+        params: serde_json::Value,
+    ) -> Result<serde_json::Value> {
+        let request_id = self.request_id.fetch_add(1, Ordering::Relaxed);
+        let request = request.build_request_json(request_id, params);
+        let mut resp = self.do_send_with_retry(request).await?;
+        Self::check_response(&resp)?;
+
+        Ok(resp["result"].take())
+    }
+
+    async fn send_batch(
+        &self,
+        requests_and_params: Vec<(RpcRequest, serde_json::Value)>,
+    ) -> Result<serde_json::Value> {
+        let mut batch_request = vec![];
+        for (request_id, req) in requests_and_params.into_iter().enumerate() {
+            batch_request.push(req.0.build_request_json(request_id as u64, req.1));
+        }
+
+        let resp = self
+            .do_send_with_retry(serde_json::Value::Array(batch_request))
+            .await?;
+
+        Ok(resp)
+    }
+
+    fn get_transport_stats(&self) -> RpcTransportStats {
+        self.stats.read().unwrap().clone()
+    }
 
     fn url(&self) -> String {
         self.url.clone()
diff --git a/rpc-client/src/mock_sender.rs b/rpc-client/src/mock_sender.rs
index 654f45d029..d710735dbc 100644
--- a/rpc-client/src/mock_sender.rs
+++ b/rpc-client/src/mock_sender.rs
@@ -489,4 +489,11 @@ impl RpcSender for MockSender {
     fn url(&self) -> String {
         format!("MockSender: {}", self.url)
     }
+
+    async fn send_batch(
+        &self,
+        _requests_and_params: Vec<(RpcRequest, serde_json::Value)>,
+    ) -> Result<Value> {
+        todo!()
+    }
 }
diff --git a/rpc-client/src/nonblocking/rpc_client.rs b/rpc-client/src/nonblocking/rpc_client.rs
index 21350938a7..145c3417e0 100644
--- a/rpc-client/src/nonblocking/rpc_client.rs
+++ b/rpc-client/src/nonblocking/rpc_client.rs
@@ -33,6 +33,10 @@ use {
         UiAccount, UiAccountData, UiAccountEncoding,
     },
     solana_rpc_client_api::{
+        bundles::{
+            RpcBundleRequest, RpcSimulateBundleConfig, RpcSimulateBundleResult,
+            SimulationSlotConfig,
+        },
         client_error::{
             Error as ClientError, ErrorKind as ClientErrorKind, Result as ClientResult,
         },
@@ -43,6 +47,7 @@ use {
     },
     solana_sdk::{
         account::Account,
+        bundle::VersionedBundle,
         clock::{Epoch, Slot, UnixTimestamp, DEFAULT_MS_PER_SLOT},
         commitment_config::{CommitmentConfig, CommitmentLevel},
         epoch_info::EpochInfo,
@@ -51,7 +56,7 @@ use {
         hash::Hash,
         pubkey::Pubkey,
         signature::Signature,
-        transaction,
+        transaction::{self, VersionedTransaction},
     },
     solana_transaction_status::{
         EncodedConfirmedBlock, EncodedConfirmedTransactionWithStatusMeta, TransactionStatus,
@@ -970,6 +975,7 @@ impl RpcClient {
                     code,
                     message,
                     data,
+                    ..
                 }) = &err.kind
                 {
                     debug!("{} {}", code, message);
@@ -1412,6 +1418,113 @@ impl RpcClient {
         .await
     }
 
+    pub async fn batch_simulate_bundle(
+        &self,
+        bundles: &[VersionedBundle],
+    ) -> BatchRpcResult<RpcSimulateBundleResult> {
+        let configs = bundles
+            .iter()
+            .map(|b| RpcSimulateBundleConfig {
+                simulation_bank: Some(SimulationSlotConfig::Commitment(self.commitment())),
+                pre_execution_accounts_configs: vec![None; b.transactions.len()],
+                post_execution_accounts_configs: vec![None; b.transactions.len()],
+                ..RpcSimulateBundleConfig::default()
+            })
+            .collect::<Vec<RpcSimulateBundleConfig>>();
+
+        self.batch_simulate_bundle_with_config(bundles.iter().zip(configs).collect())
+            .await
+    }
+
+    pub async fn batch_simulate_bundle_with_config(
+        &self,
+        bundles_and_configs: Vec<(&VersionedBundle, RpcSimulateBundleConfig)>,
+    ) -> BatchRpcResult<RpcSimulateBundleResult> {
+        let mut params = vec![];
+        for (bundle, config) in bundles_and_configs {
+            let transaction_encoding = if let Some(encoding) = config.transaction_encoding {
+                encoding
+            } else {
+                self.default_cluster_transaction_encoding().await?
+            };
+
+            let simulation_bank = config.simulation_bank.unwrap_or_default();
+
+            let config = RpcSimulateBundleConfig {
+                transaction_encoding: Some(transaction_encoding),
+                simulation_bank: Some(simulation_bank),
+                ..config
+            };
+
+            let encoded_transactions = bundle
+                .transactions
+                .iter()
+                .map(|tx| serialize_and_encode::<VersionedTransaction>(tx, transaction_encoding))
+                .collect::<Result<Vec<String>, ClientError>>()?;
+            let rpc_bundle_request = RpcBundleRequest {
+                encoded_transactions,
+            };
+
+            params.push(json!([rpc_bundle_request, config]));
+        }
+
+        let requests_and_params = vec![RpcRequest::SimulateBundle; params.len()]
+            .into_iter()
+            .zip(params)
+            .collect();
+        self.send_batch(requests_and_params).await
+    }
+
+    pub async fn simulate_bundle(
+        &self,
+        bundle: &VersionedBundle,
+    ) -> RpcResult<RpcSimulateBundleResult> {
+        self.simulate_bundle_with_config(
+            bundle,
+            RpcSimulateBundleConfig {
+                simulation_bank: Some(SimulationSlotConfig::Commitment(self.commitment())),
+                pre_execution_accounts_configs: vec![None; bundle.transactions.len()],
+                post_execution_accounts_configs: vec![None; bundle.transactions.len()],
+                ..RpcSimulateBundleConfig::default()
+            },
+        )
+        .await
+    }
+
+    pub async fn simulate_bundle_with_config(
+        &self,
+        bundle: &VersionedBundle,
+        config: RpcSimulateBundleConfig,
+    ) -> RpcResult<RpcSimulateBundleResult> {
+        let transaction_encoding = if let Some(enc) = config.transaction_encoding {
+            enc
+        } else {
+            self.default_cluster_transaction_encoding().await?
+        };
+        let simulation_bank = Some(config.simulation_bank.unwrap_or_default());
+
+        let encoded_transactions = bundle
+            .transactions
+            .iter()
+            .map(|tx| serialize_and_encode::<VersionedTransaction>(tx, transaction_encoding))
+            .collect::<ClientResult<Vec<String>>>()?;
+        let rpc_bundle_request = RpcBundleRequest {
+            encoded_transactions,
+        };
+
+        let config = RpcSimulateBundleConfig {
+            transaction_encoding: Some(transaction_encoding),
+            simulation_bank,
+            ..config
+        };
+
+        self.send(
+            RpcRequest::SimulateBundle,
+            json!([rpc_bundle_request, config]),
+        )
+        .await
+    }
+
     /// Returns the highest slot information that the node has snapshots for.
     ///
     /// This will find the highest full snapshot slot, and the highest incremental snapshot slot
@@ -5375,6 +5488,22 @@ impl RpcClient {
             .map_err(|err| ClientError::new_with_request(err.into(), request))
     }
 
+    pub async fn send_batch<T>(
+        &self,
+        requests_and_params: Vec<(RpcRequest, Value)>,
+    ) -> ClientResult<T>
+    where
+        T: serde::de::DeserializeOwned,
+    {
+        let response = self.sender.send_batch(requests_and_params).await?;
+        debug!("response: {:?}", response);
+
+        serde_json::from_value(response).map_err(|err| ClientError {
+            request: None,
+            kind: err.into(),
+        })
+    }
+
     pub fn get_transport_stats(&self) -> RpcTransportStats {
         self.sender.get_transport_stats()
     }
diff --git a/rpc-client/src/rpc_client.rs b/rpc-client/src/rpc_client.rs
index afccd7af00..33301ea73c 100644
--- a/rpc-client/src/rpc_client.rs
+++ b/rpc-client/src/rpc_client.rs
@@ -28,6 +28,7 @@ use {
         UiAccount, UiAccountEncoding,
     },
     solana_rpc_client_api::{
+        bundles::{RpcSimulateBundleConfig, RpcSimulateBundleResult},
         client_error::{Error as ClientError, ErrorKind, Result as ClientResult},
         config::{RpcAccountInfoConfig, *},
         request::{RpcRequest, TokenAccountsFilter},
@@ -35,6 +36,7 @@ use {
     },
     solana_sdk::{
         account::{Account, ReadableAccount},
+        bundle::VersionedBundle,
         clock::{Epoch, Slot, UnixTimestamp},
         commitment_config::CommitmentConfig,
         epoch_info::EpochInfo,
@@ -1151,6 +1153,34 @@ impl RpcClient {
         )
     }
 
+    pub fn batch_simulate_bundle(
+        &self,
+        bundles: &[VersionedBundle],
+    ) -> BatchRpcResult<RpcSimulateBundleResult> {
+        self.invoke(self.rpc_client.batch_simulate_bundle(bundles))
+    }
+
+    pub fn batch_simulate_bundle_with_config(
+        &self,
+        bundles_and_configs: Vec<(&VersionedBundle, RpcSimulateBundleConfig)>,
+    ) -> BatchRpcResult<RpcSimulateBundleResult> {
+        self.invoke(
+            (self.rpc_client.as_ref()).batch_simulate_bundle_with_config(bundles_and_configs),
+        )
+    }
+
+    pub fn simulate_bundle(&self, bundle: &VersionedBundle) -> RpcResult<RpcSimulateBundleResult> {
+        self.invoke((self.rpc_client.as_ref()).simulate_bundle(bundle))
+    }
+
+    pub fn simulate_bundle_with_config(
+        &self,
+        bundle: &VersionedBundle,
+        config: RpcSimulateBundleConfig,
+    ) -> RpcResult<RpcSimulateBundleResult> {
+        self.invoke((self.rpc_client.as_ref()).simulate_bundle_with_config(bundle, config))
+    }
+
     /// Returns the highest slot information that the node has snapshots for.
     ///
     /// This will find the highest full snapshot slot, and the highest incremental snapshot slot
diff --git a/rpc-client/src/rpc_sender.rs b/rpc-client/src/rpc_sender.rs
index 948ac45a46..6a357b4e6b 100644
--- a/rpc-client/src/rpc_sender.rs
+++ b/rpc-client/src/rpc_sender.rs
@@ -31,6 +31,10 @@ pub trait RpcSender {
         request: RpcRequest,
         params: serde_json::Value,
     ) -> Result<serde_json::Value>;
+    async fn send_batch(
+        &self,
+        requests_and_params: Vec<(RpcRequest, serde_json::Value)>,
+    ) -> Result<serde_json::Value>;
     fn get_transport_stats(&self) -> RpcTransportStats;
     fn url(&self) -> String;
 }
diff --git a/rpc-test/Cargo.toml b/rpc-test/Cargo.toml
index 578757a571..41724e1197 100644
--- a/rpc-test/Cargo.toml
+++ b/rpc-test/Cargo.toml
@@ -33,6 +33,7 @@ solana-transaction-status = { workspace = true }
 tokio = { version = "~1.14.1", features = ["full"] }
 
 [dev-dependencies]
+serial_test = { workspace = true }
 solana-logger = { workspace = true }
 
 [package.metadata.docs.rs]
diff --git a/rpc-test/tests/rpc.rs b/rpc-test/tests/rpc.rs
index f1c2d4acb9..e7f85e2285 100644
--- a/rpc-test/tests/rpc.rs
+++ b/rpc-test/tests/rpc.rs
@@ -5,6 +5,7 @@ use {
     log::*,
     reqwest::{self, header::CONTENT_TYPE},
     serde_json::{json, Value},
+    serial_test::serial,
     solana_account_decoder::UiAccount,
     solana_client::{
         connection_cache::ConnectionCache,
@@ -241,6 +242,7 @@ fn test_rpc_slot_updates() {
 }
 
 #[test]
+#[serial] // helps test pass
 fn test_rpc_subscriptions() {
     solana_logger::setup();
 
diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml
index 00c8c4a043..4f26d72e20 100644
--- a/rpc/Cargo.toml
+++ b/rpc/Cargo.toml
@@ -30,6 +30,7 @@ serde_derive = { workspace = true }
 serde_json = { workspace = true }
 soketto = { workspace = true }
 solana-account-decoder = { workspace = true }
+solana-bundle = { workspace = true }
 solana-client = { workspace = true }
 solana-entry = { workspace = true }
 solana-faucet = { workspace = true }
@@ -39,6 +40,7 @@ solana-measure = { workspace = true }
 solana-metrics = { workspace = true }
 solana-perf = { workspace = true }
 solana-poh = { workspace = true }
+solana-program-runtime = { workspace = true }
 solana-rayon-threadlimit = { workspace = true }
 solana-rpc-client-api = { workspace = true }
 solana-runtime = { workspace = true }
diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs
index 287aeac0c6..554850e122 100644
--- a/rpc/src/rpc.rs
+++ b/rpc/src/rpc.rs
@@ -225,6 +225,13 @@ impl JsonRpcRequestProcessor {
         Ok(bank)
     }
 
+    fn bank_from_slot(&self, slot: Slot) -> Option<Arc<Bank>> {
+        debug!("Slot: {:?}", slot);
+
+        let r_bank_forks = self.bank_forks.read().unwrap();
+        r_bank_forks.get(slot)
+    }
+
     #[allow(deprecated)]
     fn bank(&self, commitment: Option<CommitmentConfig>) -> Arc<Bank> {
         debug!("RPC commitment_config: {:?}", commitment);
@@ -359,13 +366,10 @@ impl JsonRpcRequestProcessor {
             );
             ClusterInfo::new(contact_info, keypair, socket_addr_space)
         });
-        let tpu_address = cluster_info
-            .my_contact_info()
-            .tpu(connection_cache.protocol())
-            .unwrap();
+
         let (sender, receiver) = unbounded();
         SendTransactionService::new::<NullTpuInfo>(
-            tpu_address,
+            cluster_info.clone(),
             &bank_forks,
             None,
             receiver,
@@ -2654,13 +2658,16 @@ pub mod rpc_minimal {
                 })
                 .unwrap();
 
-            let full_snapshot_slot =
-                snapshot_utils::get_highest_full_snapshot_archive_slot(full_snapshot_archives_dir)
-                    .ok_or(RpcCustomError::NoSnapshot)?;
+            let full_snapshot_slot = snapshot_utils::get_highest_full_snapshot_archive_slot(
+                full_snapshot_archives_dir,
+                None,
+            )
+            .ok_or(RpcCustomError::NoSnapshot)?;
             let incremental_snapshot_slot =
                 snapshot_utils::get_highest_incremental_snapshot_archive_slot(
                     incremental_snapshot_archives_dir,
                     full_snapshot_slot,
+                    None,
                 );
 
             Ok(RpcSnapshotSlotInfo {
@@ -3266,13 +3273,168 @@ pub mod rpc_accounts_scan {
     }
 }
 
+pub mod utils {
+    use {
+        crate::rpc::encode_account,
+        jsonrpc_core::Error,
+        solana_account_decoder::{UiAccount, UiAccountEncoding},
+        solana_bundle::{
+            bundle_execution::{LoadAndExecuteBundleError, LoadAndExecuteBundleOutput},
+            BundleExecutionError,
+        },
+        solana_rpc_client_api::{
+            bundles::{
+                RpcBundleExecutionError, RpcBundleSimulationSummary, RpcSimulateBundleConfig,
+                RpcSimulateBundleResult, RpcSimulateBundleTransactionResult,
+            },
+            config::RpcSimulateTransactionAccountsConfig,
+        },
+        solana_sdk::{account::AccountSharedData, pubkey::Pubkey},
+        std::str::FromStr,
+    };
+
+    /// Encodes the accounts, returns an error if any of the accounts failed to encode
+    /// The outer error can be set by error parsing, Ok(None) means there wasn't any accounts in the parameter
+    fn try_encode_accounts(
+        accounts: &Option<Vec<(Pubkey, AccountSharedData)>>,
+        encoding: UiAccountEncoding,
+    ) -> Result<Option<Vec<UiAccount>>, Error> {
+        if let Some(accounts) = accounts {
+            Ok(Some(
+                accounts
+                    .iter()
+                    .map(|(pubkey, account)| encode_account(account, pubkey, encoding, None))
+                    .collect::<Result<Vec<UiAccount>, Error>>()?,
+            ))
+        } else {
+            Ok(None)
+        }
+    }
+
+    pub fn rpc_bundle_result_from_bank_result(
+        bundle_execution_result: LoadAndExecuteBundleOutput,
+        rpc_config: RpcSimulateBundleConfig,
+    ) -> Result<RpcSimulateBundleResult, Error> {
+        let summary = match bundle_execution_result.result() {
+            Ok(_) => RpcBundleSimulationSummary::Succeeded,
+            Err(e) => {
+                let tx_signature = match e {
+                    LoadAndExecuteBundleError::TransactionError { signature, .. }
+                    | LoadAndExecuteBundleError::LockError { signature, .. } => {
+                        Some(signature.to_string())
+                    }
+                    _ => None,
+                };
+                RpcBundleSimulationSummary::Failed {
+                    error: RpcBundleExecutionError::from(BundleExecutionError::TransactionFailure(
+                        e.clone(),
+                    )),
+                    tx_signature,
+                }
+            }
+        };
+
+        let mut transaction_results = Vec::new();
+        for bundle_output in bundle_execution_result.bundle_transaction_results() {
+            for (index, execution_result) in bundle_output
+                .execution_results()
+                .iter()
+                .enumerate()
+                .filter(|(_, result)| result.was_executed())
+            {
+                // things are filtered by was_executed, so safe to unwrap here
+                let result = execution_result.flattened_result();
+                let details = execution_result.details().unwrap();
+
+                let account_config = rpc_config
+                    .pre_execution_accounts_configs
+                    .get(transaction_results.len())
+                    .ok_or_else(|| Error::invalid_params("the length of pre_execution_accounts_configs must match the number of transactions"))?;
+                let account_encoding = account_config
+                    .as_ref()
+                    .and_then(|config| config.encoding)
+                    .unwrap_or(UiAccountEncoding::Base64);
+
+                let pre_execution_accounts = if let Some(pre_tx_accounts) =
+                    bundle_output.pre_tx_execution_accounts().get(index)
+                {
+                    try_encode_accounts(pre_tx_accounts, account_encoding)?
+                } else {
+                    None
+                };
+
+                let post_execution_accounts = if let Some(post_tx_accounts) =
+                    bundle_output.post_tx_execution_accounts().get(index)
+                {
+                    try_encode_accounts(post_tx_accounts, account_encoding)?
+                } else {
+                    None
+                };
+
+                transaction_results.push(RpcSimulateBundleTransactionResult {
+                    err: match result {
+                        Ok(_) => None,
+                        Err(e) => Some(e),
+                    },
+                    logs: details.log_messages.clone(),
+                    pre_execution_accounts,
+                    post_execution_accounts,
+                    units_consumed: Some(details.executed_units),
+                    return_data: details.return_data.clone().map(|data| data.into()),
+                });
+            }
+        }
+
+        Ok(RpcSimulateBundleResult {
+            summary,
+            transaction_results,
+        })
+    }
+
+    pub fn account_configs_to_accounts(
+        accounts_config: &[Option<RpcSimulateTransactionAccountsConfig>],
+    ) -> Result<Vec<Option<Vec<Pubkey>>>, Error> {
+        let mut execution_accounts = Vec::new();
+        for account_config in accounts_config {
+            let accounts = match account_config {
+                None => None,
+                Some(account_config) => Some(
+                    account_config
+                        .addresses
+                        .iter()
+                        .map(|a| {
+                            Pubkey::from_str(a).map_err(|_| {
+                                Error::invalid_params(format!("invalid pubkey provided: {}", a))
+                            })
+                        })
+                        .collect::<Result<Vec<Pubkey>, Error>>()?,
+                ),
+            };
+            execution_accounts.push(accounts);
+        }
+        Ok(execution_accounts)
+    }
+}
+
 // Full RPC interface that an API node is expected to provide
 // (rpc_minimal should also be provided by an API node)
 pub mod rpc_full {
     use {
         super::*,
-        solana_sdk::message::{SanitizedVersionedMessage, VersionedMessage},
+        crate::rpc::utils::{account_configs_to_accounts, rpc_bundle_result_from_bank_result},
+        jsonrpc_core::ErrorCode,
+        solana_bundle::bundle_execution::{load_and_execute_bundle, LoadAndExecuteBundleError},
+        solana_rpc_client_api::bundles::{
+            RpcBundleRequest, RpcSimulateBundleConfig, RpcSimulateBundleResult,
+            SimulationSlotConfig,
+        },
+        solana_sdk::{
+            bundle::{derive_bundle_id, SanitizedBundle},
+            clock::MAX_PROCESSING_AGE,
+            message::{SanitizedVersionedMessage, VersionedMessage},
+        },
     };
+
     #[rpc]
     pub trait Full {
         type Metadata;
@@ -3334,6 +3496,14 @@ pub mod rpc_full {
             config: Option<RpcSimulateTransactionConfig>,
         ) -> Result<RpcResponse<RpcSimulateTransactionResult>>;
 
+        #[rpc(meta, name = "simulateBundle")]
+        fn simulate_bundle(
+            &self,
+            meta: Self::Metadata,
+            rpc_bundle_request: RpcBundleRequest,
+            config: Option<RpcSimulateBundleConfig>,
+        ) -> Result<RpcResponse<RpcSimulateBundleResult>>;
+
         #[rpc(meta, name = "minimumLedgerSlot")]
         fn minimum_ledger_slot(&self, meta: Self::Metadata) -> Result<Slot>;
 
@@ -3822,6 +3992,146 @@ pub mod rpc_full {
             ))
         }
 
+        // TODO (LB): probably want to add a max transaction size and max account return size and max
+        // allowable simulation time
+        fn simulate_bundle(
+            &self,
+            meta: Self::Metadata,
+            rpc_bundle_request: RpcBundleRequest,
+            config: Option<RpcSimulateBundleConfig>,
+        ) -> Result<RpcResponse<RpcSimulateBundleResult>> {
+            const MAX_BUNDLE_SIMULATION_TIME: Duration = Duration::from_millis(500);
+
+            debug!("simulate_bundle rpc request received");
+
+            let config = config.unwrap_or_else(|| RpcSimulateBundleConfig {
+                pre_execution_accounts_configs: vec![
+                    None;
+                    rpc_bundle_request.encoded_transactions.len()
+                ],
+                post_execution_accounts_configs: vec![
+                    None;
+                    rpc_bundle_request.encoded_transactions.len()
+                ],
+                ..RpcSimulateBundleConfig::default()
+            });
+
+            // Run some request validations
+            if !(config.pre_execution_accounts_configs.len()
+                == rpc_bundle_request.encoded_transactions.len()
+                && config.post_execution_accounts_configs.len()
+                    == rpc_bundle_request.encoded_transactions.len())
+            {
+                return Err(Error::invalid_params(
+                    "pre/post_execution_accounts_configs must be equal in length to the number of transactions",
+                ));
+            }
+
+            let bank = match config.simulation_bank.unwrap_or_default() {
+                SimulationSlotConfig::Commitment(commitment) => Ok(meta.bank(Some(commitment))),
+                SimulationSlotConfig::Slot(slot) => meta.bank_from_slot(slot).ok_or_else(|| {
+                    Error::invalid_params(format!("bank not found for the provided slot: {}", slot))
+                }),
+                SimulationSlotConfig::Tip => Ok(meta.bank_forks.read().unwrap().working_bank()),
+            }?;
+
+            let tx_encoding = config
+                .transaction_encoding
+                .unwrap_or(UiTransactionEncoding::Base64);
+            let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| {
+                Error::invalid_params(format!(
+                    "Unsupported encoding: {}. Supported encodings are: base58 & base64",
+                    tx_encoding
+                ))
+            })?;
+            let mut decoded_transactions = rpc_bundle_request
+                .encoded_transactions
+                .into_iter()
+                .map(|encoded_tx| {
+                    decode_and_deserialize::<VersionedTransaction>(encoded_tx, binary_encoding)
+                        .map(|de| de.1)
+                })
+                .collect::<Result<Vec<VersionedTransaction>>>()?;
+
+            if config.replace_recent_blockhash {
+                if !config.skip_sig_verify {
+                    return Err(Error::invalid_params(
+                        "sigVerify may not be used with replaceRecentBlockhash",
+                    ));
+                }
+                decoded_transactions.iter_mut().for_each(|tx| {
+                    tx.message.set_recent_blockhash(bank.last_blockhash());
+                });
+            }
+
+            let bundle_id = derive_bundle_id(&decoded_transactions);
+            let sanitized_bundle = SanitizedBundle {
+                transactions: decoded_transactions
+                    .into_iter()
+                    .map(|tx| sanitize_transaction(tx, bank.as_ref()))
+                    .collect::<Result<Vec<SanitizedTransaction>>>()?,
+                bundle_id,
+            };
+
+            if !config.skip_sig_verify {
+                for tx in &sanitized_bundle.transactions {
+                    verify_transaction(tx, &bank.feature_set)?;
+                }
+            }
+
+            let pre_execution_accounts =
+                account_configs_to_accounts(&config.pre_execution_accounts_configs)?;
+            let post_execution_accounts =
+                account_configs_to_accounts(&config.post_execution_accounts_configs)?;
+
+            let bundle_execution_result = load_and_execute_bundle(
+                &bank,
+                &sanitized_bundle,
+                MAX_PROCESSING_AGE,
+                &MAX_BUNDLE_SIMULATION_TIME,
+                true,
+                true,
+                true,
+                true,
+                &None,
+                true,
+                None,
+                &pre_execution_accounts,
+                &post_execution_accounts,
+            );
+
+            // only return error if irrecoverable (timeout or tx malformed)
+            // bundle execution failures w/ context are returned to client
+            match bundle_execution_result.result() {
+                Ok(()) | Err(LoadAndExecuteBundleError::TransactionError { .. }) => {}
+                Err(LoadAndExecuteBundleError::ProcessingTimeExceeded(elapsed)) => {
+                    let mut error = Error::new(ErrorCode::ServerError(10_000));
+                    error.message = format!(
+                        "simulation time exceeded max allowed time: {:?}ms",
+                        elapsed.as_millis()
+                    );
+                    return Err(error);
+                }
+                Err(LoadAndExecuteBundleError::InvalidPreOrPostAccounts) => {
+                    return Err(Error::invalid_params("invalid pre or post account data"));
+                }
+                Err(LoadAndExecuteBundleError::LockError {
+                    signature,
+                    transaction_error,
+                }) => {
+                    return Err(Error::invalid_params(format!(
+                        "error locking transaction with signature: {}, error: {:?}",
+                        signature, transaction_error
+                    )));
+                }
+            }
+
+            let rpc_bundle_result =
+                rpc_bundle_result_from_bank_result(bundle_execution_result, config)?;
+
+            Ok(new_response(&bank, rpc_bundle_result))
+        }
+
         fn minimum_ledger_slot(&self, meta: Self::Metadata) -> Result<Slot> {
             debug!("minimum_ledger_slot rpc request received");
             meta.minimum_ledger_slot()
@@ -4152,6 +4462,7 @@ pub mod rpc_deprecated_v1_9 {
                 .and_then(|snapshot_config| {
                     snapshot_utils::get_highest_full_snapshot_archive_slot(
                         snapshot_config.full_snapshot_archives_dir,
+                        None,
                     )
                 })
                 .ok_or_else(|| RpcCustomError::NoSnapshot.into())
@@ -4643,6 +4954,7 @@ pub mod tests {
             },
             rpc_subscriptions::RpcSubscriptions,
         },
+        base64::engine::general_purpose,
         bincode::deserialize,
         jsonrpc_core::{futures, ErrorCode, MetaIoHandler, Output, Response, Value},
         jsonrpc_core_client::transports::local,
@@ -5855,6 +6167,146 @@ pub mod tests {
         assert_eq!(result.len(), 0);
     }
 
+    #[test]
+    fn test_rpc_simulate_bundle_happy_path() {
+        // 1. setup
+        let rpc = RpcHandler::start();
+        let bank = rpc.working_bank();
+
+        let recent_blockhash = bank.confirmed_last_blockhash();
+        let RpcHandler {
+            ref meta, ref io, ..
+        } = rpc;
+
+        let data_len = 100;
+        let lamports = bank.get_minimum_balance_for_rent_exemption(data_len);
+        let leader_pubkey = solana_sdk::pubkey::new_rand();
+        let leader_account_data = AccountSharedData::new(lamports, data_len, &system_program::id());
+        bank.store_account(&leader_pubkey, &leader_account_data);
+        bank.freeze();
+
+        // 2. build bundle
+
+        // let's pretend the RPC keypair is a searcher
+        let searcher_keypair = rpc.mint_keypair;
+
+        // create tip tx
+        let tip_amount = 10000;
+        let tip_tx = VersionedTransaction::from(system_transaction::transfer(
+            &searcher_keypair,
+            &leader_pubkey,
+            tip_amount,
+            recent_blockhash,
+        ));
+
+        // some random mev tx
+        let mev_amount = 20000;
+        let goku_pubkey = solana_sdk::pubkey::new_rand();
+        let mev_tx = VersionedTransaction::from(system_transaction::transfer(
+            &searcher_keypair,
+            &goku_pubkey,
+            mev_amount,
+            recent_blockhash,
+        ));
+
+        let encoded_mev_tx = general_purpose::STANDARD.encode(serialize(&mev_tx).unwrap());
+        let encoded_tip_tx = general_purpose::STANDARD.encode(serialize(&tip_tx).unwrap());
+        let b64_data = general_purpose::STANDARD.encode(leader_account_data.data());
+
+        // 3. test and assert
+        let skip_sig_verify = true;
+        let replace_recent_blockhash = false;
+        let expected_response = json!({
+            "jsonrpc": "2.0",
+            "result": {
+                "context": {"slot": bank.slot(), "apiVersion": RpcApiVersion::default()},
+                "value":{
+                    "summary": "succeeded",
+                    "transactionResults": [
+                        {
+                            "err": null,
+                            "logs": ["Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success"],
+                            "returnData": null,
+                            "unitsConsumed": 150,
+                            "postExecutionAccounts": [],
+                            "preExecutionAccounts": [
+                                {
+                                    "data": [b64_data, "base64"],
+                                    "executable": false,
+                                    "lamports": leader_account_data.lamports(),
+                                    "owner": "11111111111111111111111111111111",
+                                    "rentEpoch": 0,
+                                    "space": 100
+                                }
+                            ],
+                        },
+                        {
+                            "err": null,
+                            "logs": ["Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success"],
+                            "returnData": null,
+                            "unitsConsumed": 150,
+                            "preExecutionAccounts": [],
+                            "postExecutionAccounts": [
+                                {
+                                    "data": [b64_data, "base64"],
+                                    "executable": false,
+                                    "lamports": leader_account_data.lamports() + tip_amount,
+                                    "owner": "11111111111111111111111111111111",
+                                    "rentEpoch": u64::MAX,
+                                    "space": 100
+                                }
+                            ],
+                        },
+                    ],
+                }
+            },
+            "id": 1,
+        });
+
+        let request = format!(
+            r#"{{"jsonrpc":"2.0",
+                 "id":1,
+                 "method":"simulateBundle",
+                 "params":[
+                   {{
+                     "encodedTransactions": ["{}", "{}"]
+                   }},
+                   {{
+                     "skipSigVerify": {},
+                     "replaceRecentBlockhash": {},
+                     "slot": {},
+                     "preExecutionAccountsConfigs": [
+                        {{ "encoding": "base64", "addresses": ["{}"] }},
+                        {{ "encoding": "base64", "addresses": [] }}
+                     ],
+                     "postExecutionAccountsConfigs": [
+                        {{ "encoding": "base64", "addresses": [] }},
+                        {{ "encoding": "base64", "addresses": ["{}"] }}
+                     ]
+                   }}
+                ]
+            }}"#,
+            encoded_mev_tx,
+            encoded_tip_tx,
+            skip_sig_verify,
+            replace_recent_blockhash,
+            bank.slot(),
+            leader_pubkey,
+            leader_pubkey,
+        );
+
+        let actual_response = io
+            .handle_request_sync(&request, meta.clone())
+            .expect("response");
+
+        let expected_response = serde_json::from_value::<Response>(expected_response)
+            .expect("expected_response deserialization");
+        let actual_response = serde_json::from_str::<Response>(&actual_response)
+            .expect("actual_response deserialization");
+
+        assert_eq!(expected_response, actual_response);
+    }
+
     #[test]
     fn test_rpc_simulate_transaction() {
         let rpc = RpcHandler::start();
@@ -6439,10 +6891,7 @@ pub mod tests {
             ClusterInfo::new(contact_info, keypair, SocketAddrSpace::Unspecified)
         });
         let connection_cache = Arc::new(ConnectionCache::new("connection_cache_test"));
-        let tpu_address = cluster_info
-            .my_contact_info()
-            .tpu(connection_cache.protocol())
-            .unwrap();
+
         let (meta, receiver) = JsonRpcRequestProcessor::new(
             JsonRpcConfig::default(),
             None,
@@ -6451,7 +6900,7 @@ pub mod tests {
             blockstore,
             validator_exit,
             health.clone(),
-            cluster_info,
+            cluster_info.clone(),
             Hash::default(),
             None,
             optimistically_confirmed_bank,
@@ -6463,7 +6912,7 @@ pub mod tests {
             Arc::new(PrioritizationFeeCache::default()),
         );
         SendTransactionService::new::<NullTpuInfo>(
-            tpu_address,
+            cluster_info,
             &bank_forks,
             None,
             receiver,
@@ -6711,12 +7160,9 @@ pub mod tests {
 
         let cluster_info = Arc::new(new_test_cluster_info());
         let connection_cache = Arc::new(ConnectionCache::new("connection_cache_test"));
-        let tpu_address = cluster_info
-            .my_contact_info()
-            .tpu(connection_cache.protocol())
-            .unwrap();
         let optimistically_confirmed_bank =
             OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks);
+
         let (request_processor, receiver) = JsonRpcRequestProcessor::new(
             JsonRpcConfig::default(),
             None,
@@ -6725,7 +7171,7 @@ pub mod tests {
             blockstore.clone(),
             validator_exit,
             RpcHealth::stub(optimistically_confirmed_bank.clone(), blockstore),
-            cluster_info,
+            cluster_info.clone(),
             Hash::default(),
             None,
             optimistically_confirmed_bank,
@@ -6737,7 +7183,7 @@ pub mod tests {
             Arc::new(PrioritizationFeeCache::default()),
         );
         SendTransactionService::new::<NullTpuInfo>(
-            tpu_address,
+            cluster_info,
             &bank_forks,
             None,
             receiver,
diff --git a/rpc/src/rpc_service.rs b/rpc/src/rpc_service.rs
index 0e84d4ca48..3020f899c8 100644
--- a/rpc/src/rpc_service.rs
+++ b/rpc/src/rpc_service.rs
@@ -255,6 +255,7 @@ impl RequestMiddleware for RpcRequestMiddleware {
                 let full_snapshot_archive_info =
                     snapshot_utils::get_highest_full_snapshot_archive_info(
                         &snapshot_config.full_snapshot_archives_dir,
+                        None,
                     );
                 let snapshot_archive_info =
                     if let Some(full_snapshot_archive_info) = full_snapshot_archive_info {
@@ -264,6 +265,7 @@ impl RequestMiddleware for RpcRequestMiddleware {
                             snapshot_utils::get_highest_incremental_snapshot_archive_info(
                                 &snapshot_config.incremental_snapshot_archives_dir,
                                 full_snapshot_archive_info.slot(),
+                                None,
                             )
                             .map(|incremental_snapshot_archive_info| {
                                 incremental_snapshot_archive_info
@@ -377,11 +379,6 @@ impl JsonRpcService {
             LARGEST_ACCOUNTS_CACHE_DURATION,
         )));
 
-        let tpu_address = cluster_info
-            .my_contact_info()
-            .tpu(connection_cache.protocol())
-            .map_err(|err| format!("{err}"))?;
-
         // sadly, some parts of our current rpc implemention block the jsonrpc's
         // _socket-listening_ event loop for too long, due to (blocking) long IO or intesive CPU,
         // causing no further processing of incoming requests and ultimatily innocent clients timing-out.
@@ -478,7 +475,7 @@ impl JsonRpcService {
         let leader_info =
             poh_recorder.map(|recorder| ClusterTpuInfo::new(cluster_info.clone(), recorder));
         let _send_transaction_service = Arc::new(SendTransactionService::new_with_config(
-            tpu_address,
+            cluster_info,
             &bank_forks,
             leader_info,
             receiver,
diff --git a/runtime-plugin/Cargo.toml b/runtime-plugin/Cargo.toml
new file mode 100644
index 0000000000..531bcf69fe
--- /dev/null
+++ b/runtime-plugin/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "solana-runtime-plugin"
+description = "Solana runtime plugin"
+version = { workspace = true }
+authors = { workspace = true }
+repository = { workspace = true }
+homepage = { workspace = true }
+license = { workspace = true }
+edition = { workspace = true }
+
+[dependencies]
+crossbeam-channel = { workspace = true }
+json5 = { workspace = true }
+jsonrpc-core = { workspace = true }
+jsonrpc-core-client = { workspace = true, features = ["ipc"] }
+jsonrpc-derive = { workspace = true }
+jsonrpc-ipc-server = { workspace = true }
+jsonrpc-server-utils = { workspace = true }
+libloading = { workspace = true }
+log = { workspace = true }
+solana-runtime = { workspace = true }
+solana-sdk = { workspace = true }
+thiserror = { workspace = true }
diff --git a/runtime-plugin/src/lib.rs b/runtime-plugin/src/lib.rs
new file mode 100644
index 0000000000..477af43c9b
--- /dev/null
+++ b/runtime-plugin/src/lib.rs
@@ -0,0 +1,4 @@
+pub mod runtime_plugin;
+pub mod runtime_plugin_admin_rpc_service;
+pub mod runtime_plugin_manager;
+pub mod runtime_plugin_service;
diff --git a/runtime-plugin/src/runtime_plugin.rs b/runtime-plugin/src/runtime_plugin.rs
new file mode 100644
index 0000000000..7dc0b95fa4
--- /dev/null
+++ b/runtime-plugin/src/runtime_plugin.rs
@@ -0,0 +1,41 @@
+use {
+    solana_runtime::{bank_forks::BankForks, commitment::BlockCommitmentCache},
+    std::{
+        any::Any,
+        error,
+        fmt::Debug,
+        io,
+        sync::{atomic::AtomicBool, Arc, RwLock},
+    },
+    thiserror::Error,
+};
+
+pub type Result<T> = std::result::Result<T, RuntimePluginError>;
+
+/// Errors returned by plugin calls
+#[derive(Error, Debug)]
+pub enum RuntimePluginError {
+    /// Error opening the configuration file; for example, when the file
+    /// is not found or when the validator process has no permission to read it.
+    #[error("Error opening config file. Error detail: ({0}).")]
+    ConfigFileOpenError(#[from] io::Error),
+
+    /// Any custom error defined by the plugin.
+    #[error("Plugin-defined custom error. Error message: ({0})")]
+    Custom(Box<dyn error::Error + Send + Sync>),
+
+    #[error("Failed to load a runtime plugin")]
+    FailedToLoadPlugin(#[from] Box<dyn std::error::Error>),
+}
+
+pub struct PluginDependencies {
+    pub bank_forks: Arc<RwLock<BankForks>>,
+    pub block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
+    pub exit: Arc<AtomicBool>,
+}
+
+pub trait RuntimePlugin: Any + Debug + Send + Sync {
+    fn name(&self) -> &'static str;
+    fn on_load(&mut self, config_file: &str, dependencies: PluginDependencies) -> Result<()>;
+    fn on_unload(&mut self);
+}
diff --git a/runtime-plugin/src/runtime_plugin_admin_rpc_service.rs b/runtime-plugin/src/runtime_plugin_admin_rpc_service.rs
new file mode 100644
index 0000000000..fdc33b06c5
--- /dev/null
+++ b/runtime-plugin/src/runtime_plugin_admin_rpc_service.rs
@@ -0,0 +1,326 @@
+//! RPC interface to dynamically make changes to runtime plugins.
+
+use {
+    crossbeam_channel::Sender,
+    jsonrpc_core::{BoxFuture, ErrorCode, MetaIoHandler, Metadata, Result as JsonRpcResult},
+    jsonrpc_core_client::{transports::ipc, RpcError},
+    jsonrpc_derive::rpc,
+    jsonrpc_ipc_server::{
+        tokio::{self, sync::oneshot::channel as oneshot_channel},
+        RequestContext, ServerBuilder,
+    },
+    jsonrpc_server_utils::tokio::sync::oneshot::Sender as OneShotSender,
+    log::*,
+    solana_sdk::exit::Exit,
+    std::{
+        path::{Path, PathBuf},
+        sync::{
+            atomic::{AtomicBool, Ordering},
+            Arc, RwLock,
+        },
+    },
+};
+
+#[derive(Debug)]
+pub enum RuntimePluginManagerRpcRequest {
+    ReloadPlugin {
+        name: String,
+        config_file: String,
+        response_sender: OneShotSender<JsonRpcResult<()>>,
+    },
+    UnloadPlugin {
+        name: String,
+        response_sender: OneShotSender<JsonRpcResult<()>>,
+    },
+    LoadPlugin {
+        config_file: String,
+        response_sender: OneShotSender<JsonRpcResult<String>>,
+    },
+    ListPlugins {
+        response_sender: OneShotSender<JsonRpcResult<Vec<String>>>,
+    },
+}
+
+#[rpc]
+pub trait RuntimePluginAdminRpc {
+    type Metadata;
+
+    #[rpc(meta, name = "reloadPlugin")]
+    fn reload_plugin(
+        &self,
+        meta: Self::Metadata,
+        name: String,
+        config_file: String,
+    ) -> BoxFuture<JsonRpcResult<()>>;
+
+    #[rpc(meta, name = "unloadPlugin")]
+    fn unload_plugin(&self, meta: Self::Metadata, name: String) -> BoxFuture<JsonRpcResult<()>>;
+
+    #[rpc(meta, name = "loadPlugin")]
+    fn load_plugin(
+        &self,
+        meta: Self::Metadata,
+        config_file: String,
+    ) -> BoxFuture<JsonRpcResult<String>>;
+
+    #[rpc(meta, name = "listPlugins")]
+    fn list_plugins(&self, meta: Self::Metadata) -> BoxFuture<JsonRpcResult<Vec<String>>>;
+}
+
+#[derive(Clone)]
+pub struct RuntimePluginAdminRpcRequestMetadata {
+    pub rpc_request_sender: Sender<RuntimePluginManagerRpcRequest>,
+    pub validator_exit: Arc<RwLock<Exit>>,
+}
+
+impl Metadata for RuntimePluginAdminRpcRequestMetadata {}
+
+fn rpc_path(ledger_path: &Path) -> PathBuf {
+    #[cfg(target_family = "windows")]
+    {
+        // More information about the wackiness of pipe names over at
+        // https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-names
+        if let Some(ledger_filename) = ledger_path.file_name() {
+            PathBuf::from(format!(
+                "\\\\.\\pipe\\{}-runtime_plugin_admin.rpc",
+                ledger_filename.to_string_lossy()
+            ))
+        } else {
+            PathBuf::from("\\\\.\\pipe\\runtime_plugin_admin.rpc")
+        }
+    }
+    #[cfg(not(target_family = "windows"))]
+    {
+        ledger_path.join("runtime_plugin_admin.rpc")
+    }
+}
+
+/// Start the Runtime Plugin Admin RPC interface.
+pub fn run(
+    ledger_path: &Path,
+    metadata: RuntimePluginAdminRpcRequestMetadata,
+    plugin_exit: Arc<AtomicBool>,
+) {
+    let rpc_path = rpc_path(ledger_path);
+
+    let event_loop = tokio::runtime::Builder::new_multi_thread()
+        .thread_name("solRuntimePluginAdminRpc")
+        .worker_threads(1)
+        .enable_all()
+        .build()
+        .unwrap();
+
+    std::thread::Builder::new()
+        .name("solAdminRpc".to_string())
+        .spawn(move || {
+            let mut io = MetaIoHandler::default();
+            io.extend_with(RuntimePluginAdminRpcImpl.to_delegate());
+
+            let validator_exit = metadata.validator_exit.clone();
+
+            match ServerBuilder::with_meta_extractor(io, move |_req: &RequestContext| {
+                metadata.clone()
+            })
+            .event_loop_executor(event_loop.handle().clone())
+            .start(&format!("{}", rpc_path.display()))
+            {
+                Err(e) => {
+                    error!("Unable to start runtime plugin admin rpc service: {e:?}, exiting");
+                    validator_exit.write().unwrap().exit();
+                }
+                Ok(server) => {
+                    info!("started runtime plugin admin rpc service!");
+                    let close_handle = server.close_handle();
+                    let c_plugin_exit = plugin_exit.clone();
+                    validator_exit
+                        .write()
+                        .unwrap()
+                        .register_exit(Box::new(move || {
+                            close_handle.close();
+                            c_plugin_exit.store(true, Ordering::Relaxed);
+                        }));
+
+                    server.wait();
+                    plugin_exit.store(true, Ordering::Relaxed);
+                }
+            }
+        })
+        .unwrap();
+}
+
+pub struct RuntimePluginAdminRpcImpl;
+impl RuntimePluginAdminRpc for RuntimePluginAdminRpcImpl {
+    type Metadata = RuntimePluginAdminRpcRequestMetadata;
+
+    fn reload_plugin(
+        &self,
+        meta: Self::Metadata,
+        name: String,
+        config_file: String,
+    ) -> BoxFuture<JsonRpcResult<()>> {
+        Box::pin(async move {
+            let (response_sender, response_receiver) = oneshot_channel();
+
+            if meta
+                .rpc_request_sender
+                .send(RuntimePluginManagerRpcRequest::ReloadPlugin {
+                    name,
+                    config_file,
+                    response_sender,
+                })
+                .is_err()
+            {
+                error!("rpc_request_sender channel closed, exiting");
+                meta.validator_exit.write().unwrap().exit();
+
+                return Err(jsonrpc_core::Error {
+                    code: ErrorCode::InternalError,
+                    message: "Internal channel disconnected while sending the request".to_string(),
+                    data: None,
+                });
+            }
+
+            match response_receiver.await {
+                Err(_) => {
+                    error!("response_receiver channel closed, exiting");
+                    meta.validator_exit.write().unwrap().exit();
+                    Err(jsonrpc_core::Error {
+                        code: ErrorCode::InternalError,
+                        message: "Internal channel disconnected while awaiting the response"
+                            .to_string(),
+                        data: None,
+                    })
+                }
+                Ok(resp) => resp,
+            }
+        })
+    }
+
+    fn unload_plugin(&self, meta: Self::Metadata, name: String) -> BoxFuture<JsonRpcResult<()>> {
+        Box::pin(async move {
+            let (response_sender, response_receiver) = oneshot_channel();
+
+            if meta
+                .rpc_request_sender
+                .send(RuntimePluginManagerRpcRequest::UnloadPlugin {
+                    name,
+                    response_sender,
+                })
+                .is_err()
+            {
+                error!("rpc_request_sender channel closed, exiting");
+                meta.validator_exit.write().unwrap().exit();
+
+                return Err(jsonrpc_core::Error {
+                    code: ErrorCode::InternalError,
+                    message: "Internal channel disconnected while sending the request".to_string(),
+                    data: None,
+                });
+            }
+
+            match response_receiver.await {
+                Err(_) => {
+                    error!("response_receiver channel closed, exiting");
+                    meta.validator_exit.write().unwrap().exit();
+                    Err(jsonrpc_core::Error {
+                        code: ErrorCode::InternalError,
+                        message: "Internal channel disconnected while awaiting the response"
+                            .to_string(),
+                        data: None,
+                    })
+                }
+                Ok(resp) => resp,
+            }
+        })
+    }
+
+    fn load_plugin(
+        &self,
+        meta: Self::Metadata,
+        config_file: String,
+    ) -> BoxFuture<JsonRpcResult<String>> {
+        Box::pin(async move {
+            let (response_sender, response_receiver) = oneshot_channel();
+
+            if meta
+                .rpc_request_sender
+                .send(RuntimePluginManagerRpcRequest::LoadPlugin {
+                    config_file,
+                    response_sender,
+                })
+                .is_err()
+            {
+                error!("rpc_request_sender channel closed, exiting");
+                meta.validator_exit.write().unwrap().exit();
+
+                return Err(jsonrpc_core::Error {
+                    code: ErrorCode::InternalError,
+                    message: "Internal channel disconnected while sending the request".to_string(),
+                    data: None,
+                });
+            }
+
+            match response_receiver.await {
+                Err(_) => {
+                    error!("response_receiver channel closed, exiting");
+                    meta.validator_exit.write().unwrap().exit();
+                    Err(jsonrpc_core::Error {
+                        code: ErrorCode::InternalError,
+                        message: "Internal channel disconnected while awaiting the response"
+                            .to_string(),
+                        data: None,
+                    })
+                }
+                Ok(resp) => resp,
+            }
+        })
+    }
+
+    fn list_plugins(&self, meta: Self::Metadata) -> BoxFuture<JsonRpcResult<Vec<String>>> {
+        Box::pin(async move {
+            let (response_sender, response_receiver) = oneshot_channel();
+
+            if meta
+                .rpc_request_sender
+                .send(RuntimePluginManagerRpcRequest::ListPlugins { response_sender })
+                .is_err()
+            {
+                error!("rpc_request_sender channel closed, exiting");
+                meta.validator_exit.write().unwrap().exit();
+
+                return Err(jsonrpc_core::Error {
+                    code: ErrorCode::InternalError,
+                    message: "Internal channel disconnected while sending the request".to_string(),
+                    data: None,
+                });
+            }
+
+            match response_receiver.await {
+                Err(_) => {
+                    error!("response_receiver channel closed, exiting");
+                    meta.validator_exit.write().unwrap().exit();
+                    Err(jsonrpc_core::Error {
+                        code: ErrorCode::InternalError,
+                        message: "Internal channel disconnected while awaiting the response"
+                            .to_string(),
+                        data: None,
+                    })
+                }
+                Ok(resp) => resp,
+            }
+        })
+    }
+}
+
+// Connect to the Runtime Plugin RPC interface
+pub async fn connect(ledger_path: &Path) -> Result<gen_client::Client, RpcError> {
+    let rpc_path = rpc_path(ledger_path);
+    if !rpc_path.exists() {
+        Err(RpcError::Client(format!(
+            "{} does not exist",
+            rpc_path.display()
+        )))
+    } else {
+        ipc::connect::<_, gen_client::Client>(&format!("{}", rpc_path.display())).await
+    }
+}
diff --git a/runtime-plugin/src/runtime_plugin_manager.rs b/runtime-plugin/src/runtime_plugin_manager.rs
new file mode 100644
index 0000000000..af1dcf2cde
--- /dev/null
+++ b/runtime-plugin/src/runtime_plugin_manager.rs
@@ -0,0 +1,275 @@
+use {
+    crate::runtime_plugin::{PluginDependencies, RuntimePlugin},
+    jsonrpc_core::{serde_json, ErrorCode, Result as JsonRpcResult},
+    libloading::Library,
+    log::*,
+    solana_runtime::{bank_forks::BankForks, commitment::BlockCommitmentCache},
+    std::{
+        fs::File,
+        io::Read,
+        path::{Path, PathBuf},
+        sync::{atomic::AtomicBool, Arc, RwLock},
+    },
+};
+
+#[derive(thiserror::Error, Debug)]
+pub enum RuntimePluginManagerError {
+    #[error("Cannot open the the plugin config file")]
+    CannotOpenConfigFile(String),
+
+    #[error("Cannot read the the plugin config file")]
+    CannotReadConfigFile(String),
+
+    #[error("The config file is not in a valid Json format")]
+    InvalidConfigFileFormat(String),
+
+    #[error("Plugin library path is not specified in the config file")]
+    LibPathNotSet,
+
+    #[error("Invalid plugin path")]
+    InvalidPluginPath,
+
+    #[error("Cannot load plugin shared library")]
+    PluginLoadError(String),
+
+    #[error("The runtime plugin {0} is already loaded shared library")]
+    PluginAlreadyLoaded(String),
+
+    #[error("The RuntimePlugin on_load method failed")]
+    PluginStartError(String),
+}
+
+pub struct RuntimePluginManager {
+    plugins: Vec<Box<dyn RuntimePlugin>>,
+    libs: Vec<Library>,
+    bank_forks: Arc<RwLock<BankForks>>,
+    block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
+    exit: Arc<AtomicBool>,
+}
+
+impl RuntimePluginManager {
+    pub fn new(
+        bank_forks: Arc<RwLock<BankForks>>,
+        block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
+        exit: Arc<AtomicBool>,
+    ) -> Self {
+        Self {
+            plugins: vec![],
+            libs: vec![],
+            bank_forks,
+            block_commitment_cache,
+            exit,
+        }
+    }
+
+    /// This method allows dynamic loading of a runtime plugin.
+    /// Adds to the existing list of loaded plugins.
+    pub(crate) fn load_plugin(
+        &mut self,
+        plugin_config_path: impl AsRef<Path>,
+    ) -> JsonRpcResult<String /* plugin name */> {
+        // First load plugin
+        let (mut new_plugin, new_lib, config_file) =
+            load_plugin_from_config(plugin_config_path.as_ref()).map_err(|e| {
+                jsonrpc_core::Error {
+                    code: ErrorCode::InvalidRequest,
+                    message: format!("Failed to load plugin: {e}"),
+                    data: None,
+                }
+            })?;
+
+        // Then see if a plugin with this name already exists, if so return Err.
+        let name = new_plugin.name();
+        if self.plugins.iter().any(|plugin| name.eq(plugin.name())) {
+            return Err(jsonrpc_core::Error {
+                code: ErrorCode::InvalidRequest,
+                message: format!(
+                    "There already exists a plugin named {} loaded. Did not load requested plugin",
+                    name,
+                ),
+                data: None,
+            });
+        }
+
+        new_plugin
+            .on_load(
+                config_file,
+                PluginDependencies {
+                    bank_forks: self.bank_forks.clone(),
+                    block_commitment_cache: self.block_commitment_cache.clone(),
+                    exit: self.exit.clone(),
+                },
+            )
+            .map_err(|on_load_err| jsonrpc_core::Error {
+                code: ErrorCode::InvalidRequest,
+                message: format!(
+                    "on_load method of plugin {} failed: {on_load_err}",
+                    new_plugin.name()
+                ),
+                data: None,
+            })?;
+
+        self.plugins.push(new_plugin);
+        self.libs.push(new_lib);
+
+        Ok(name.to_string())
+    }
+
+    /// Unloads the plugins and loaded plugin libraries, making sure to fire
+    /// their `on_plugin_unload()` methods so they can do any necessary cleanup.
+    pub(crate) fn unload_all_plugins(&mut self) {
+        (0..self.plugins.len()).for_each(|idx| {
+            self.try_drop_plugin(idx);
+        });
+    }
+
+    pub(crate) fn unload_plugin(&mut self, name: &str) -> JsonRpcResult<()> {
+        // Check if any plugin names match this one
+        let Some(idx) = self
+            .plugins
+            .iter()
+            .position(|plugin| plugin.name().eq(name))
+        else {
+            // If we don't find one return an error
+            return Err(jsonrpc_core::error::Error {
+                code: ErrorCode::InvalidRequest,
+                message: String::from("The plugin you requested to unload is not loaded"),
+                data: None,
+            });
+        };
+
+        // Unload and drop plugin and lib
+        self.try_drop_plugin(idx);
+
+        Ok(())
+    }
+
+    /// Reloads an existing plugin.
+    pub(crate) fn reload_plugin(&mut self, name: &str, config_file: &str) -> JsonRpcResult<()> {
+        // Check if any plugin names match this one
+        let Some(idx) = self
+            .plugins
+            .iter()
+            .position(|plugin| plugin.name().eq(name))
+        else {
+            // If we don't find one return an error
+            return Err(jsonrpc_core::error::Error {
+                code: ErrorCode::InvalidRequest,
+                message: String::from("The plugin you requested to reload is not loaded"),
+                data: None,
+            });
+        };
+
+        self.try_drop_plugin(idx);
+
+        // Try to load plugin, library
+        // SAFETY: It is up to the validator to ensure this is a valid plugin library.
+        let (mut new_plugin, new_lib, new_parsed_config_file) =
+            load_plugin_from_config(config_file.as_ref()).map_err(|err| jsonrpc_core::Error {
+                code: ErrorCode::InvalidRequest,
+                message: err.to_string(),
+                data: None,
+            })?;
+
+        // Attempt to on_load with new plugin
+        match new_plugin.on_load(
+            new_parsed_config_file,
+            PluginDependencies {
+                bank_forks: self.bank_forks.clone(),
+                block_commitment_cache: self.block_commitment_cache.clone(),
+                exit: self.exit.clone(),
+            },
+        ) {
+            // On success, push plugin and library
+            Ok(()) => {
+                self.plugins.push(new_plugin);
+                self.libs.push(new_lib);
+                Ok(())
+            }
+            // On failure, return error
+            Err(err) => Err(jsonrpc_core::error::Error {
+                code: ErrorCode::InvalidRequest,
+                message: format!(
+                    "Failed to start new plugin (previous plugin was dropped!): {err}"
+                ),
+                data: None,
+            }),
+        }
+    }
+
+    pub(crate) fn list_plugins(&self) -> JsonRpcResult<Vec<String>> {
+        Ok(self.plugins.iter().map(|p| p.name().to_owned()).collect())
+    }
+
+    fn try_drop_plugin(&mut self, idx: usize) {
+        if idx < self.plugins.len() {
+            let mut plugin = self.plugins.remove(idx);
+            let lib = self.libs.remove(idx);
+            drop(lib);
+            plugin.on_unload();
+        } else {
+            error!("failed to drop plugin: index {idx} out of bounds");
+        }
+    }
+}
+
+fn load_plugin_from_config(
+    plugin_config_path: &Path,
+) -> Result<(Box<dyn RuntimePlugin>, Library, &str), RuntimePluginManagerError> {
+    type PluginConstructor = unsafe fn() -> *mut dyn RuntimePlugin;
+    use libloading::Symbol;
+
+    let mut file = match File::open(plugin_config_path) {
+        Ok(file) => file,
+        Err(err) => {
+            return Err(RuntimePluginManagerError::CannotOpenConfigFile(format!(
+                "Failed to open the plugin config file {plugin_config_path:?}, error: {err:?}"
+            )));
+        }
+    };
+
+    let mut contents = String::new();
+    if let Err(err) = file.read_to_string(&mut contents) {
+        return Err(RuntimePluginManagerError::CannotReadConfigFile(format!(
+            "Failed to read the plugin config file {plugin_config_path:?}, error: {err:?}"
+        )));
+    }
+
+    let result: serde_json::Value = match json5::from_str(&contents) {
+        Ok(value) => value,
+        Err(err) => {
+            return Err(RuntimePluginManagerError::InvalidConfigFileFormat(format!(
+                "The config file {plugin_config_path:?} is not in a valid Json5 format, error: {err:?}"
+            )));
+        }
+    };
+
+    let libpath = result["libpath"]
+        .as_str()
+        .ok_or(RuntimePluginManagerError::LibPathNotSet)?;
+    let mut libpath = PathBuf::from(libpath);
+    if libpath.is_relative() {
+        let config_dir = plugin_config_path.parent().ok_or_else(|| {
+            RuntimePluginManagerError::CannotOpenConfigFile(format!(
+                "Failed to resolve parent of {plugin_config_path:?}",
+            ))
+        })?;
+        libpath = config_dir.join(libpath);
+    }
+
+    let config_file = plugin_config_path
+        .as_os_str()
+        .to_str()
+        .ok_or(RuntimePluginManagerError::InvalidPluginPath)?;
+
+    let (plugin, lib) = unsafe {
+        let lib = Library::new(libpath)
+            .map_err(|e| RuntimePluginManagerError::PluginLoadError(e.to_string()))?;
+        let constructor: Symbol<PluginConstructor> = lib
+            .get(b"_create_plugin")
+            .map_err(|e| RuntimePluginManagerError::PluginLoadError(e.to_string()))?;
+        (Box::from_raw(constructor()), lib)
+    };
+
+    Ok((plugin, lib, config_file))
+}
diff --git a/runtime-plugin/src/runtime_plugin_service.rs b/runtime-plugin/src/runtime_plugin_service.rs
new file mode 100644
index 0000000000..5fcb625a26
--- /dev/null
+++ b/runtime-plugin/src/runtime_plugin_service.rs
@@ -0,0 +1,123 @@
+use {
+    crate::{
+        runtime_plugin::RuntimePluginError,
+        runtime_plugin_admin_rpc_service::RuntimePluginManagerRpcRequest,
+        runtime_plugin_manager::RuntimePluginManager,
+    },
+    crossbeam_channel::Receiver,
+    log::{error, info},
+    solana_runtime::{bank_forks::BankForks, commitment::BlockCommitmentCache},
+    std::{
+        path::PathBuf,
+        sync::{
+            atomic::{AtomicBool, Ordering},
+            Arc, RwLock,
+        },
+        thread::{self, JoinHandle},
+        time::Duration,
+    },
+};
+
+pub struct RuntimePluginService {
+    plugin_manager: Arc<RwLock<RuntimePluginManager>>,
+    rpc_thread: JoinHandle<()>,
+}
+
+impl RuntimePluginService {
+    pub fn start(
+        plugin_config_files: &[PathBuf],
+        rpc_receiver: Receiver<RuntimePluginManagerRpcRequest>,
+        bank_forks: Arc<RwLock<BankForks>>,
+        block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
+        exit: Arc<AtomicBool>,
+    ) -> Result<Self, RuntimePluginError> {
+        let mut plugin_manager =
+            RuntimePluginManager::new(bank_forks, block_commitment_cache, exit.clone());
+
+        for config in plugin_config_files {
+            let name = plugin_manager
+                .load_plugin(config)
+                .map_err(|e| RuntimePluginError::FailedToLoadPlugin(e.into()))?;
+            info!("Loaded Runtime Plugin: {name}");
+        }
+
+        let plugin_manager = Arc::new(RwLock::new(plugin_manager));
+        let rpc_thread =
+            Self::start_rpc_request_handler(rpc_receiver, plugin_manager.clone(), exit);
+
+        Ok(Self {
+            plugin_manager,
+            rpc_thread,
+        })
+    }
+
+    pub fn join(self) {
+        if let Err(e) = self.rpc_thread.join() {
+            error!("error joining rpc thread: {e:?}");
+        }
+        self.plugin_manager.write().unwrap().unload_all_plugins();
+    }
+
+    fn start_rpc_request_handler(
+        rpc_receiver: Receiver<RuntimePluginManagerRpcRequest>,
+        plugin_manager: Arc<RwLock<RuntimePluginManager>>,
+        exit: Arc<AtomicBool>,
+    ) -> JoinHandle<()> {
+        thread::Builder::new()
+            .name("solRuntimePluginRpc".to_string())
+            .spawn(move || {
+                const TIMEOUT: Duration = Duration::from_secs(3);
+                while !exit.load(Ordering::Relaxed) {
+                    if let Ok(request) = rpc_receiver.recv_timeout(TIMEOUT) {
+                        match request {
+                            RuntimePluginManagerRpcRequest::ListPlugins { response_sender } => {
+                                let plugin_list = plugin_manager.read().unwrap().list_plugins();
+                                if response_sender.send(plugin_list).is_err() {
+                                    error!("response_sender channel disconnected");
+                                    return;
+                                }
+                            }
+                            RuntimePluginManagerRpcRequest::ReloadPlugin {
+                                ref name,
+                                ref config_file,
+                                response_sender,
+                            } => {
+                                let reload_result = plugin_manager
+                                    .write()
+                                    .unwrap()
+                                    .reload_plugin(name, config_file);
+                                if response_sender.send(reload_result).is_err() {
+                                    error!("response_sender channel disconnected");
+                                    return;
+                                }
+                            }
+                            RuntimePluginManagerRpcRequest::LoadPlugin {
+                                ref config_file,
+                                response_sender,
+                            } => {
+                                let load_result =
+                                    plugin_manager.write().unwrap().load_plugin(config_file);
+                                if response_sender.send(load_result).is_err() {
+                                    error!("response_sender channel disconnected");
+                                    return;
+                                }
+                            }
+                            RuntimePluginManagerRpcRequest::UnloadPlugin {
+                                ref name,
+                                response_sender,
+                            } => {
+                                let unload_result =
+                                    plugin_manager.write().unwrap().unload_plugin(name);
+                                if response_sender.send(unload_result).is_err() {
+                                    error!("response_sender channel disconnected");
+                                    return;
+                                }
+                            }
+                        }
+                    }
+                }
+                plugin_manager.write().unwrap().unload_all_plugins();
+            })
+            .unwrap()
+    }
+}
diff --git a/runtime/src/account_overrides.rs b/runtime/src/account_overrides.rs
index ee8e7ec9e2..d5d3286426 100644
--- a/runtime/src/account_overrides.rs
+++ b/runtime/src/account_overrides.rs
@@ -4,12 +4,16 @@ use {
 };
 
 /// Encapsulates overridden accounts, typically used for transaction simulations
-#[derive(Default)]
+#[derive(Clone, Default)]
 pub struct AccountOverrides {
     accounts: HashMap<Pubkey, AccountSharedData>,
 }
 
 impl AccountOverrides {
+    pub fn upsert_account_overrides(&mut self, other: AccountOverrides) {
+        self.accounts.extend(other.accounts);
+    }
+
     pub fn set_account(&mut self, pubkey: &Pubkey, account: Option<AccountSharedData>) {
         match account {
             Some(account) => self.accounts.insert(*pubkey, account),
diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs
index 7a7bb7212e..06e2f986cd 100644
--- a/runtime/src/accounts.rs
+++ b/runtime/src/accounts.rs
@@ -1168,19 +1168,24 @@ impl Accounts {
     }
 
     fn lock_account(
-        &self,
         account_locks: &mut AccountLocks,
         writable_keys: Vec<&Pubkey>,
         readonly_keys: Vec<&Pubkey>,
+        additional_read_locks: &HashSet<Pubkey>,
+        additional_write_locks: &HashSet<Pubkey>,
     ) -> Result<()> {
         for k in writable_keys.iter() {
-            if account_locks.is_locked_write(k) || account_locks.is_locked_readonly(k) {
+            if account_locks.is_locked_write(k)
+                || account_locks.is_locked_readonly(k)
+                || additional_write_locks.contains(k)
+                || additional_read_locks.contains(k)
+            {
                 debug!("Writable account in use: {:?}", k);
                 return Err(TransactionError::AccountInUse);
             }
         }
         for k in readonly_keys.iter() {
-            if account_locks.is_locked_write(k) {
+            if account_locks.is_locked_write(k) || additional_write_locks.contains(k) {
                 debug!("Read-only account in use: {:?}", k);
                 return Err(TransactionError::AccountInUse);
             }
@@ -1225,7 +1230,22 @@ impl Accounts {
         let tx_account_locks_results: Vec<Result<_>> = txs
             .map(|tx| tx.get_account_locks(tx_account_lock_limit))
             .collect();
-        self.lock_accounts_inner(tx_account_locks_results)
+        self.lock_accounts_inner(
+            tx_account_locks_results,
+            &HashSet::default(),
+            &HashSet::default(),
+        )
+    }
+
+    pub fn lock_accounts_sequential_with_results<'a>(
+        &self,
+        txs: impl Iterator<Item = &'a SanitizedTransaction>,
+        tx_account_lock_limit: usize,
+    ) -> Vec<Result<()>> {
+        let tx_account_locks_results: Vec<Result<_>> = txs
+            .map(|tx| tx.get_account_locks(tx_account_lock_limit))
+            .collect();
+        self.lock_accounts_sequential_inner(tx_account_locks_results)
     }
 
     #[must_use]
@@ -1235,6 +1255,8 @@ impl Accounts {
         txs: impl Iterator<Item = &'a SanitizedTransaction>,
         results: impl Iterator<Item = Result<()>>,
         tx_account_lock_limit: usize,
+        additional_read_locks: &HashSet<Pubkey>,
+        additional_write_locks: &HashSet<Pubkey>,
     ) -> Vec<Result<()>> {
         let tx_account_locks_results: Vec<Result<_>> = txs
             .zip(results)
@@ -1243,28 +1265,74 @@ impl Accounts {
                 Err(err) => Err(err),
             })
             .collect();
-        self.lock_accounts_inner(tx_account_locks_results)
+        self.lock_accounts_inner(
+            tx_account_locks_results,
+            additional_read_locks,
+            additional_write_locks,
+        )
     }
 
     #[must_use]
     fn lock_accounts_inner(
         &self,
         tx_account_locks_results: Vec<Result<TransactionAccountLocks>>,
+        additional_read_locks: &HashSet<Pubkey>,
+        additional_write_locks: &HashSet<Pubkey>,
     ) -> Vec<Result<()>> {
         let account_locks = &mut self.account_locks.lock().unwrap();
         tx_account_locks_results
             .into_iter()
             .map(|tx_account_locks_result| match tx_account_locks_result {
-                Ok(tx_account_locks) => self.lock_account(
+                Ok(tx_account_locks) => Self::lock_account(
                     account_locks,
                     tx_account_locks.writable,
                     tx_account_locks.readonly,
+                    additional_read_locks,
+                    additional_write_locks,
                 ),
                 Err(err) => Err(err),
             })
             .collect()
     }
 
+    #[must_use]
+    fn lock_accounts_sequential_inner(
+        &self,
+        tx_account_locks_results: Vec<Result<TransactionAccountLocks>>,
+    ) -> Vec<Result<()>> {
+        let mut l_account_locks = self.account_locks.lock().unwrap();
+        Self::lock_accounts_sequential(&mut l_account_locks, tx_account_locks_results)
+    }
+
+    pub fn lock_accounts_sequential(
+        account_locks: &mut AccountLocks,
+        tx_account_locks_results: Vec<Result<TransactionAccountLocks>>,
+    ) -> Vec<Result<()>> {
+        let mut account_in_use_set = false;
+        tx_account_locks_results
+            .into_iter()
+            .map(|tx_account_locks_result| match tx_account_locks_result {
+                Ok(tx_account_locks) => match account_in_use_set {
+                    true => Err(TransactionError::AccountInUse),
+                    false => {
+                        let locked = Self::lock_account(
+                            account_locks,
+                            tx_account_locks.writable,
+                            tx_account_locks.readonly,
+                            &HashSet::default(),
+                            &HashSet::default(),
+                        );
+                        if matches!(locked, Err(TransactionError::AccountInUse)) {
+                            account_in_use_set = true;
+                        }
+                        locked
+                    }
+                },
+                Err(err) => Err(err),
+            })
+            .collect()
+    }
+
     /// Once accounts are unlocked, new transactions that modify that state can enter the pipeline
     #[allow(clippy::needless_collect)]
     pub fn unlock_accounts<'a>(
@@ -1308,7 +1376,7 @@ impl Accounts {
         lamports_per_signature: u64,
         include_slot_in_hash: IncludeSlotInHash,
     ) {
-        let (accounts_to_store, transactions) = self.collect_accounts_to_store(
+        let (accounts_to_store, transactions) = Self::collect_accounts_to_store(
             txs,
             res,
             loaded,
@@ -1335,8 +1403,7 @@ impl Accounts {
     }
 
     #[allow(clippy::too_many_arguments)]
-    fn collect_accounts_to_store<'a>(
-        &self,
+    pub fn collect_accounts_to_store<'a>(
         txs: &'a [SanitizedTransaction],
         execution_results: &'a [TransactionExecutionResult],
         load_results: &'a mut [TransactionLoadResult],
@@ -1493,6 +1560,7 @@ mod tests {
             sync::atomic::{AtomicBool, AtomicU64, Ordering},
             thread, time,
         },
+        Accounts,
     };
 
     fn new_sanitized_tx<T: Signers>(
@@ -3164,6 +3232,8 @@ mod tests {
             txs.iter(),
             qos_results.into_iter(),
             MAX_TX_ACCOUNT_LOCKS,
+            &HashSet::default(),
+            &HashSet::default(),
         );
 
         assert!(results[0].is_ok()); // Read-only account (keypair0) can be referenced multiple times
@@ -3285,7 +3355,7 @@ mod tests {
         }
         let txs = vec![tx0.clone(), tx1.clone()];
         let execution_results = vec![new_execution_result(Ok(()), None); 2];
-        let (collected_accounts, transactions) = accounts.collect_accounts_to_store(
+        let (collected_accounts, transactions) = Accounts::collect_accounts_to_store(
             &txs,
             &execution_results,
             loaded.as_mut_slice(),
@@ -3749,7 +3819,7 @@ mod tests {
         let mut loaded = vec![loaded];
 
         let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique());
-        let accounts = Accounts::new_with_config_for_tests(
+        let _accounts = Accounts::new_with_config_for_tests(
             Vec::new(),
             &ClusterType::Development,
             AccountSecondaryIndexes::default(),
@@ -3763,7 +3833,7 @@ mod tests {
             )),
             nonce.as_ref(),
         )];
-        let (collected_accounts, _) = accounts.collect_accounts_to_store(
+        let (collected_accounts, _) = Accounts::collect_accounts_to_store(
             &txs,
             &execution_results,
             loaded.as_mut_slice(),
@@ -3862,7 +3932,7 @@ mod tests {
         let mut loaded = vec![loaded];
 
         let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique());
-        let accounts = Accounts::new_with_config_for_tests(
+        let _accounts = Accounts::new_with_config_for_tests(
             Vec::new(),
             &ClusterType::Development,
             AccountSecondaryIndexes::default(),
@@ -3876,7 +3946,7 @@ mod tests {
             )),
             nonce.as_ref(),
         )];
-        let (collected_accounts, _) = accounts.collect_accounts_to_store(
+        let (collected_accounts, _) = Accounts::collect_accounts_to_store(
             &txs,
             &execution_results,
             loaded.as_mut_slice(),
diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs
index eb31085c3a..bbc780e8f0 100644
--- a/runtime/src/bank.rs
+++ b/runtime/src/bank.rs
@@ -259,6 +259,7 @@ pub struct BankRc {
     pub(crate) bank_id_generator: Arc<AtomicU64>,
 }
 
+use crate::accounts::AccountLocks;
 #[cfg(RUSTC_WITH_SPECIALIZATION)]
 use solana_frozen_abi::abi_example::AbiExample;
 
@@ -358,6 +359,7 @@ impl TransactionExecutionResult {
     }
 }
 
+#[derive(Debug)]
 pub struct LoadAndExecuteTransactionsOutput {
     pub loaded_transactions: Vec<TransactionLoadResult>,
     // Vector of results indicating whether a transaction was executed or could not
@@ -399,6 +401,22 @@ impl DurableNonceFee {
     }
 }
 
+#[derive(Clone, Debug, PartialEq)]
+pub struct AccountData {
+    pub pubkey: Pubkey,
+    pub data: AccountSharedData,
+}
+
+#[derive(Clone)]
+pub struct BundleTransactionSimulationResult {
+    pub result: Result<()>,
+    pub logs: TransactionLogMessages,
+    pub pre_execution_accounts: Option<Vec<AccountData>>,
+    pub post_execution_accounts: Option<Vec<AccountData>>,
+    pub return_data: Option<TransactionReturnData>,
+    pub units_consumed: u64,
+}
+
 pub struct TransactionSimulationResult {
     pub result: Result<()>,
     pub logs: TransactionLogMessages,
@@ -406,6 +424,7 @@ pub struct TransactionSimulationResult {
     pub units_consumed: u64,
     pub return_data: Option<TransactionReturnData>,
 }
+
 pub struct TransactionBalancesSet {
     pub pre_balances: TransactionBalances,
     pub post_balances: TransactionBalances,
@@ -1004,7 +1023,7 @@ pub struct Bank {
     inflation: Arc<RwLock<Inflation>>,
 
     /// cache of vote_account and stake_account state for this fork
-    stakes_cache: StakesCache,
+    pub stakes_cache: StakesCache,
 
     /// staked nodes on epoch boundaries, saved off when a bank.slot() is at
     ///   a leader schedule calculation boundary
@@ -3630,17 +3649,61 @@ impl Bank {
         &'a self,
         transactions: &'b [SanitizedTransaction],
         transaction_results: impl Iterator<Item = Result<()>>,
+        additional_read_locks: &HashSet<Pubkey>,
+        additional_write_locks: &HashSet<Pubkey>,
     ) -> TransactionBatch<'a, 'b> {
-        // this lock_results could be: Ok, AccountInUse, WouldExceedBlockMaxLimit or WouldExceedAccountMaxLimit
         let tx_account_lock_limit = self.get_transaction_account_lock_limit();
         let lock_results = self.rc.accounts.lock_accounts_with_results(
             transactions.iter(),
             transaction_results,
             tx_account_lock_limit,
+            additional_read_locks,
+            additional_write_locks,
         );
         TransactionBatch::new(lock_results, self, Cow::Borrowed(transactions))
     }
 
+    /// Prepare a locked transaction batch from a list of sanitized transactions, and their cost
+    /// limited packing status, where transactions will be locked sequentially until the first failure
+    pub fn prepare_sequential_sanitized_batch_with_results<'a, 'b>(
+        &'a self,
+        transactions: &'b [SanitizedTransaction],
+    ) -> TransactionBatch<'a, 'b> {
+        // this lock_results could be: Ok, AccountInUse, AccountLoadedTwice, or TooManyAccountLocks
+        let tx_account_lock_limit = self.get_transaction_account_lock_limit();
+        let lock_results = self
+            .rc
+            .accounts
+            .lock_accounts_sequential_with_results(transactions.iter(), tx_account_lock_limit);
+        TransactionBatch::new(lock_results, self, Cow::Borrowed(transactions))
+    }
+
+    /// Prepare a locked transaction batch from a list of sanitized transactions for simulation.
+    /// This grabs as many sequential account locks that it can without a RW conflict. However,
+    /// it uses a temporary version of AccountLocks and not the Bank's account locks, so one can
+    /// use this during simulation on an unfrozen Bank without worrying about impacting the RW
+    /// lock usage in replay
+    pub fn prepare_sequential_sanitized_batch_with_results_for_simulation<'a, 'b>(
+        &'a self,
+        transactions: &'b [SanitizedTransaction],
+    ) -> TransactionBatch<'a, 'b> {
+        let tx_account_lock_limit = self.get_transaction_account_lock_limit();
+        let tx_account_locks_results: Vec<Result<_>> = transactions
+            .iter()
+            .map(|tx| tx.get_account_locks(tx_account_lock_limit))
+            .collect();
+
+        let mut account_locks = AccountLocks::default();
+        let lock_results =
+            Accounts::lock_accounts_sequential(&mut account_locks, tx_account_locks_results);
+        let mut batch = TransactionBatch::new(lock_results, self, Cow::Borrowed(transactions));
+        // this is required to ensure that accounts aren't unlocked accidentally, which can be problematic during replay.
+        // more specifically, during process_entries, if the lock counts are accidentally decremented,
+        // one might end up replaying a block incorrectly
+        batch.set_needs_unlock(false);
+        batch
+    }
+
     /// Prepare a transaction batch without locking accounts for transaction simulation.
     pub(crate) fn prepare_simulation_batch(
         &self,
@@ -3953,6 +4016,29 @@ impl Bank {
         }
     }
 
+    pub fn collect_balances_with_cache(
+        &self,
+        batch: &TransactionBatch,
+        account_overrides: Option<&AccountOverrides>,
+    ) -> TransactionBalances {
+        let mut balances: TransactionBalances = vec![];
+        for transaction in batch.sanitized_transactions() {
+            let mut transaction_balances: Vec<u64> = vec![];
+            for account_key in transaction.message().account_keys().iter() {
+                let balance = match account_overrides {
+                    None => self.get_balance(account_key),
+                    Some(overrides) => match overrides.get(account_key) {
+                        None => self.get_balance(account_key),
+                        Some(account_data) => account_data.lamports(),
+                    },
+                };
+                transaction_balances.push(balance);
+            }
+            balances.push(transaction_balances);
+        }
+        balances
+    }
+
     pub fn load_program(&self, pubkey: &Pubkey) -> Arc<LoadedProgram> {
         let program = if let Some(program) = self.get_account_with_fixed_root(pubkey) {
             program
@@ -5062,6 +5148,26 @@ impl Bank {
         }
     }
 
+    pub fn collect_accounts_to_store<'a>(
+        &self,
+        txs: &'a [SanitizedTransaction],
+        res: &'a [TransactionExecutionResult],
+        loaded: &'a mut [TransactionLoadResult],
+    ) -> Vec<(&'a Pubkey, &'a AccountSharedData)> {
+        let (last_blockhash, lamports_per_signature) =
+            self.last_blockhash_and_lamports_per_signature();
+        let durable_nonce = DurableNonce::from_blockhash(&last_blockhash);
+        Accounts::collect_accounts_to_store(
+            txs,
+            res,
+            loaded,
+            &self.rent_collector,
+            &durable_nonce,
+            lamports_per_signature,
+        )
+        .0
+    }
+
     // Distribute collected rent fees for this slot to staked validators (excluding stakers)
     // according to stake.
     //
diff --git a/runtime/src/cost_tracker.rs b/runtime/src/cost_tracker.rs
index 08991a854c..d360d9445a 100644
--- a/runtime/src/cost_tracker.rs
+++ b/runtime/src/cost_tracker.rs
@@ -107,6 +107,10 @@ impl CostTracker {
         self.vote_cost_limit = vote_cost_limit;
     }
 
+    pub fn set_block_cost_limit(&mut self, new_limit: u64) {
+        self.block_cost_limit = new_limit;
+    }
+
     pub fn try_add(&mut self, tx_cost: &TransactionCost) -> Result<u64, CostTrackerError> {
         self.would_fit(tx_cost)?;
         self.add_transaction_cost(tx_cost);
@@ -144,6 +148,10 @@ impl CostTracker {
         self.block_cost
     }
 
+    pub fn block_cost_limit(&self) -> u64 {
+        self.block_cost_limit
+    }
+
     pub fn transaction_count(&self) -> u64 {
         self.transaction_count
     }
diff --git a/runtime/src/snapshot_package.rs b/runtime/src/snapshot_package.rs
index 7044ce8496..6abcd4efd3 100644
--- a/runtime/src/snapshot_package.rs
+++ b/runtime/src/snapshot_package.rs
@@ -230,10 +230,14 @@ pub struct SnapshotPackage {
 impl SnapshotPackage {
     pub fn new(accounts_package: AccountsPackage, accounts_hash: AccountsHashEnum) -> Self {
         let AccountsPackageType::Snapshot(snapshot_type) = accounts_package.package_type else {
-            panic!("The AccountsPackage must be of type Snapshot in order to make a SnapshotPackage!");
+            panic!(
+                "The AccountsPackage must be of type Snapshot in order to make a SnapshotPackage!"
+            );
         };
         let Some(snapshot_info) = accounts_package.snapshot_info else {
-            panic!("The AccountsPackage must have snapshot info in order to make a SnapshotPackage!");
+            panic!(
+                "The AccountsPackage must have snapshot info in order to make a SnapshotPackage!"
+            );
         };
         let snapshot_hash =
             SnapshotHash::new(&accounts_hash, snapshot_info.epoch_accounts_hash.as_ref());
diff --git a/runtime/src/snapshot_utils.rs b/runtime/src/snapshot_utils.rs
index e76de3c636..dd6f69cd04 100644
--- a/runtime/src/snapshot_utils.rs
+++ b/runtime/src/snapshot_utils.rs
@@ -1487,12 +1487,13 @@ pub fn bank_fields_from_snapshot_archives(
     incremental_snapshot_archives_dir: impl AsRef<Path>,
 ) -> Result<BankFieldsToDeserialize> {
     let full_snapshot_archive_info =
-        get_highest_full_snapshot_archive_info(&full_snapshot_archives_dir)
+        get_highest_full_snapshot_archive_info(&full_snapshot_archives_dir, None)
             .ok_or(SnapshotError::NoSnapshotArchives)?;
 
     let incremental_snapshot_archive_info = get_highest_incremental_snapshot_archive_info(
         &incremental_snapshot_archives_dir,
         full_snapshot_archive_info.slot(),
+        None,
     );
 
     let temp_unpack_dir = TempDir::new()?;
@@ -1661,18 +1662,20 @@ pub fn bank_from_latest_snapshot_archives(
     accounts_db_config: Option<AccountsDbConfig>,
     accounts_update_notifier: Option<AccountsUpdateNotifier>,
     exit: &Arc<AtomicBool>,
+    halt_at_slot: Option<Slot>,
 ) -> Result<(
     Bank,
     FullSnapshotArchiveInfo,
     Option<IncrementalSnapshotArchiveInfo>,
 )> {
     let full_snapshot_archive_info =
-        get_highest_full_snapshot_archive_info(&full_snapshot_archives_dir)
+        get_highest_full_snapshot_archive_info(&full_snapshot_archives_dir, halt_at_slot)
             .ok_or(SnapshotError::NoSnapshotArchives)?;
 
     let incremental_snapshot_archive_info = get_highest_incremental_snapshot_archive_info(
         &incremental_snapshot_archives_dir,
         full_snapshot_archive_info.slot(),
+        halt_at_slot,
     );
 
     info!(
@@ -2366,8 +2369,9 @@ pub fn get_incremental_snapshot_archives(
 /// Get the highest slot of the full snapshot archives in a directory
 pub fn get_highest_full_snapshot_archive_slot(
     full_snapshot_archives_dir: impl AsRef<Path>,
+    halt_at_slot: Option<Slot>,
 ) -> Option<Slot> {
-    get_highest_full_snapshot_archive_info(full_snapshot_archives_dir)
+    get_highest_full_snapshot_archive_info(full_snapshot_archives_dir, halt_at_slot)
         .map(|full_snapshot_archive_info| full_snapshot_archive_info.slot())
 }
 
@@ -2376,10 +2380,12 @@ pub fn get_highest_full_snapshot_archive_slot(
 pub fn get_highest_incremental_snapshot_archive_slot(
     incremental_snapshot_archives_dir: impl AsRef<Path>,
     full_snapshot_slot: Slot,
+    halt_at_slot: Option<Slot>,
 ) -> Option<Slot> {
     get_highest_incremental_snapshot_archive_info(
         incremental_snapshot_archives_dir,
         full_snapshot_slot,
+        halt_at_slot,
     )
     .map(|incremental_snapshot_archive_info| incremental_snapshot_archive_info.slot())
 }
@@ -2387,8 +2393,13 @@ pub fn get_highest_incremental_snapshot_archive_slot(
 /// Get the path (and metadata) for the full snapshot archive with the highest slot in a directory
 pub fn get_highest_full_snapshot_archive_info(
     full_snapshot_archives_dir: impl AsRef<Path>,
+    halt_at_slot: Option<Slot>,
 ) -> Option<FullSnapshotArchiveInfo> {
     let mut full_snapshot_archives = get_full_snapshot_archives(full_snapshot_archives_dir);
+    if let Some(halt_at_slot) = halt_at_slot {
+        full_snapshot_archives
+            .retain(|archive| archive.snapshot_archive_info().slot <= halt_at_slot);
+    }
     full_snapshot_archives.sort_unstable();
     full_snapshot_archives.into_iter().rev().next()
 }
@@ -2398,6 +2409,7 @@ pub fn get_highest_full_snapshot_archive_info(
 pub fn get_highest_incremental_snapshot_archive_info(
     incremental_snapshot_archives_dir: impl AsRef<Path>,
     full_snapshot_slot: Slot,
+    halt_at_slot: Option<Slot>,
 ) -> Option<IncrementalSnapshotArchiveInfo> {
     // Since we want to filter down to only the incremental snapshot archives that have the same
     // full snapshot slot as the value passed in, perform the filtering before sorting to avoid
@@ -2409,6 +2421,9 @@ pub fn get_highest_incremental_snapshot_archive_info(
                 incremental_snapshot_archive_info.base_slot() == full_snapshot_slot
             })
             .collect::<Vec<_>>();
+    if let Some(halt_at_slot) = halt_at_slot {
+        incremental_snapshot_archives.retain(|archive| archive.slot() <= halt_at_slot);
+    }
     incremental_snapshot_archives.sort_unstable();
     incremental_snapshot_archives.into_iter().rev().next()
 }
@@ -4031,7 +4046,7 @@ mod tests {
         );
 
         assert_eq!(
-            get_highest_full_snapshot_archive_slot(full_snapshot_archives_dir.path()),
+            get_highest_full_snapshot_archive_slot(full_snapshot_archives_dir.path(), None),
             Some(max_slot - 1)
         );
     }
@@ -4057,7 +4072,8 @@ mod tests {
             assert_eq!(
                 get_highest_incremental_snapshot_archive_slot(
                     incremental_snapshot_archives_dir.path(),
-                    full_snapshot_slot
+                    full_snapshot_slot,
+                    None,
                 ),
                 Some(max_incremental_snapshot_slot - 1)
             );
@@ -4066,7 +4082,8 @@ mod tests {
         assert_eq!(
             get_highest_incremental_snapshot_archive_slot(
                 incremental_snapshot_archives_dir.path(),
-                max_full_snapshot_slot
+                max_full_snapshot_slot,
+                None,
             ),
             None
         );
@@ -4774,6 +4791,7 @@ mod tests {
             Some(ACCOUNTS_DB_CONFIG_FOR_TESTING),
             None,
             &Arc::default(),
+            None,
         )
         .unwrap();
         deserialized_bank.wait_for_initial_accounts_hash_verification_completed_for_tests();
diff --git a/runtime/src/stake_account.rs b/runtime/src/stake_account.rs
index e4cd79e48c..7dc5bf7fd0 100644
--- a/runtime/src/stake_account.rs
+++ b/runtime/src/stake_account.rs
@@ -41,14 +41,14 @@ impl<T> StakeAccount<T> {
     }
 
     #[inline]
-    pub(crate) fn stake_state(&self) -> &StakeState {
+    pub fn stake_state(&self) -> &StakeState {
         &self.stake_state
     }
 }
 
 impl StakeAccount<Delegation> {
     #[inline]
-    pub(crate) fn delegation(&self) -> Delegation {
+    pub fn delegation(&self) -> Delegation {
         // Safe to unwrap here because StakeAccount<Delegation> will always
         // only wrap a stake-state which is a delegation.
         self.stake_state.delegation().unwrap()
diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs
index 5836073b95..7d943da5fb 100644
--- a/runtime/src/stakes.rs
+++ b/runtime/src/stakes.rs
@@ -50,17 +50,17 @@ pub enum InvalidCacheEntryReason {
     WrongOwner,
 }
 
-type StakeAccount = stake_account::StakeAccount<Delegation>;
+pub type StakeAccount = stake_account::StakeAccount<Delegation>;
 
 #[derive(Default, Debug, AbiExample)]
-pub(crate) struct StakesCache(RwLock<Stakes<StakeAccount>>);
+pub struct StakesCache(RwLock<Stakes<StakeAccount>>);
 
 impl StakesCache {
     pub(crate) fn new(stakes: Stakes<StakeAccount>) -> Self {
         Self(RwLock::new(stakes))
     }
 
-    pub(crate) fn stakes(&self) -> RwLockReadGuard<Stakes<StakeAccount>> {
+    pub fn stakes(&self) -> RwLockReadGuard<Stakes<StakeAccount>> {
         self.0.read().unwrap()
     }
 
@@ -174,7 +174,7 @@ pub struct Stakes<T: Clone> {
     vote_accounts: VoteAccounts,
 
     /// stake_delegations
-    stake_delegations: ImHashMap<Pubkey, T>,
+    pub stake_delegations: ImHashMap<Pubkey, T>,
 
     /// unused
     unused: u64,
@@ -214,7 +214,7 @@ impl Stakes<StakeAccount> {
     /// full account state for respective stake pubkeys. get_account function
     /// should return the account at the respective slot where stakes where
     /// cached.
-    pub(crate) fn new<F>(stakes: &Stakes<Delegation>, get_account: F) -> Result<Self, Error>
+    pub fn new<F>(stakes: &Stakes<Delegation>, get_account: F) -> Result<Self, Error>
     where
         F: Fn(&Pubkey) -> Option<AccountSharedData>,
     {
@@ -445,7 +445,7 @@ impl Stakes<StakeAccount> {
         }
     }
 
-    pub(crate) fn stake_delegations(&self) -> &ImHashMap<Pubkey, StakeAccount> {
+    pub fn stake_delegations(&self) -> &ImHashMap<Pubkey, StakeAccount> {
         &self.stake_delegations
     }
 
diff --git a/runtime/src/transaction_batch.rs b/runtime/src/transaction_batch.rs
index 5d55acb2ec..d41a5763c2 100644
--- a/runtime/src/transaction_batch.rs
+++ b/runtime/src/transaction_batch.rs
@@ -1,6 +1,6 @@
 use {
     crate::bank::Bank,
-    solana_sdk::transaction::{Result, SanitizedTransaction},
+    solana_sdk::transaction::{Result, SanitizedTransaction, TransactionError},
     std::borrow::Cow,
 };
 
@@ -46,6 +46,28 @@ impl<'a, 'b> TransactionBatch<'a, 'b> {
     pub fn needs_unlock(&self) -> bool {
         self.needs_unlock
     }
+
+    /// Bundle locking failed if lock result returns something other than ok or AccountInUse
+    pub fn check_bundle_lock_results(&self) -> Option<(&SanitizedTransaction, &TransactionError)> {
+        self.sanitized_transactions()
+            .iter()
+            .zip(self.lock_results.iter())
+            .find(|(_, lock_result)| {
+                !matches!(lock_result, Ok(()) | Err(TransactionError::AccountInUse))
+            })
+            .map(|(transaction, lock_result)| {
+                (
+                    transaction,
+                    match lock_result {
+                        Ok(_) => {
+                            // safe here bc the above find will never return Ok
+                            unreachable!()
+                        }
+                        Err(lock_error) => lock_error,
+                    },
+                )
+            })
+    }
 }
 
 // Unlock all locked accounts in destructor.
diff --git a/rustfmt.toml b/rustfmt.toml
index e26d07f0d8..c7ccd48750 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,2 +1,7 @@
 imports_granularity = "One"
 group_imports = "One"
+
+ignore = [
+    "jito-programs",
+    "anchor"
+]
\ No newline at end of file
diff --git a/s b/s
new file mode 100755
index 0000000000..308133d227
--- /dev/null
+++ b/s
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
+
+if [ -f .env ]; then
+  export $(cat .env | grep -v '#' | awk '/=/ {print $1}')
+else
+  echo "Missing .env file"
+  exit 0
+fi
+
+echo "Syncing to host: $HOST"
+
+# sync to build server, ignoring local builds and local/remote dev ledger
+rsync -avh --delete --exclude target --exclude docker-output "$SCRIPT_DIR" "$HOST":~/
diff --git a/scripts/increment-cargo-version.sh b/scripts/increment-cargo-version.sh
index 1cadfc4bdd..c383f244dd 100755
--- a/scripts/increment-cargo-version.sh
+++ b/scripts/increment-cargo-version.sh
@@ -23,6 +23,8 @@ ignores=(
   .cargo
   target
   node_modules
+  jito-programs
+  anchor
 )
 
 not_paths=()
diff --git a/scripts/run.sh b/scripts/run.sh
index a890aa10c1..5bdf46a2d1 100755
--- a/scripts/run.sh
+++ b/scripts/run.sh
@@ -102,6 +102,10 @@ args=(
   --identity "$validator_identity"
   --vote-account "$validator_vote_account"
   --ledger "$ledgerDir"
+  --tip-payment-program-pubkey "T1pyyaTNZsKv2WcRAB8oVnk93mLJw2XzjtVYqCsaHqt"
+  --tip-distribution-program-pubkey "4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7"
+  --merkle-root-upload-authority "$validator_identity"
+  --commission-bps 0
   --gossip-port 8001
   --full-rpc-api
   --rpc-port 8899
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index 7e8368d82a..59c88e6531 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -37,6 +37,7 @@ full = [
 ]
 
 [dependencies]
+anchor-lang = { workspace = true }
 assert_matches = { workspace = true, optional = true }
 base64 = { workspace = true }
 bincode = { workspace = true }
diff --git a/sdk/src/bundle/mod.rs b/sdk/src/bundle/mod.rs
new file mode 100644
index 0000000000..3c02a59f9f
--- /dev/null
+++ b/sdk/src/bundle/mod.rs
@@ -0,0 +1,33 @@
+#![cfg(feature = "full")]
+
+use {
+    crate::transaction::{SanitizedTransaction, VersionedTransaction},
+    digest::Digest,
+    itertools::Itertools,
+    sha2::Sha256,
+};
+
+#[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize)]
+pub struct VersionedBundle {
+    pub transactions: Vec<VersionedTransaction>,
+}
+
+#[derive(Clone, Debug)]
+pub struct SanitizedBundle {
+    pub transactions: Vec<SanitizedTransaction>,
+    pub bundle_id: String,
+}
+
+pub fn derive_bundle_id(transactions: &[VersionedTransaction]) -> String {
+    let mut hasher = Sha256::new();
+    hasher.update(transactions.iter().map(|tx| tx.signatures[0]).join(","));
+    format!("{:x}", hasher.finalize())
+}
+
+pub fn derive_bundle_id_from_sanitized_transactions(
+    transactions: &[SanitizedTransaction],
+) -> String {
+    let mut hasher = Sha256::new();
+    hasher.update(transactions.iter().map(|tx| tx.signature()).join(","));
+    format!("{:x}", hasher.finalize())
+}
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
index ef73fe83c2..9f8efdb2cd 100644
--- a/sdk/src/lib.rs
+++ b/sdk/src/lib.rs
@@ -57,6 +57,7 @@ pub use solana_program::{
 
 pub mod account;
 pub mod account_utils;
+pub mod bundle;
 pub mod client;
 pub mod commitment_config;
 pub mod compute_budget;
diff --git a/send-transaction-service/Cargo.toml b/send-transaction-service/Cargo.toml
index 71431037f5..247fd4d950 100644
--- a/send-transaction-service/Cargo.toml
+++ b/send-transaction-service/Cargo.toml
@@ -13,6 +13,7 @@ edition = { workspace = true }
 crossbeam-channel = { workspace = true }
 log = { workspace = true }
 solana-client = { workspace = true }
+solana-gossip = { workspace = true }
 solana-measure = { workspace = true }
 solana-metrics = { workspace = true }
 solana-runtime = { workspace = true }
@@ -21,6 +22,7 @@ solana-tpu-client = { workspace = true }
 
 [dev-dependencies]
 solana-logger = { workspace = true }
+solana-streamer = { workspace = true }
 
 [package.metadata.docs.rs]
 targets = ["x86_64-unknown-linux-gnu"]
diff --git a/send-transaction-service/src/send_transaction_service.rs b/send-transaction-service/src/send_transaction_service.rs
index 6d5faef296..e312ed0739 100644
--- a/send-transaction-service/src/send_transaction_service.rs
+++ b/send-transaction-service/src/send_transaction_service.rs
@@ -6,6 +6,7 @@ use {
         connection_cache::{ConnectionCache, Protocol},
         tpu_connection::TpuConnection,
     },
+    solana_gossip::cluster_info::ClusterInfo,
     solana_measure::measure::Measure,
     solana_metrics::datapoint_warn,
     solana_runtime::{bank::Bank, bank_forks::BankForks},
@@ -328,7 +329,7 @@ const SEND_TRANSACTION_METRICS_REPORT_RATE_MS: u64 = 5000;
 
 impl SendTransactionService {
     pub fn new<T: TpuInfo + std::marker::Send + 'static>(
-        tpu_address: SocketAddr,
+        cluster_info: Arc<ClusterInfo>,
         bank_forks: &Arc<RwLock<BankForks>>,
         leader_info: Option<T>,
         receiver: Receiver<TransactionInfo>,
@@ -343,7 +344,7 @@ impl SendTransactionService {
             ..Config::default()
         };
         Self::new_with_config(
-            tpu_address,
+            cluster_info,
             bank_forks,
             leader_info,
             receiver,
@@ -354,7 +355,7 @@ impl SendTransactionService {
     }
 
     pub fn new_with_config<T: TpuInfo + std::marker::Send + 'static>(
-        tpu_address: SocketAddr,
+        cluster_info: Arc<ClusterInfo>,
         bank_forks: &Arc<RwLock<BankForks>>,
         leader_info: Option<T>,
         receiver: Receiver<TransactionInfo>,
@@ -369,7 +370,7 @@ impl SendTransactionService {
         let leader_info_provider = Arc::new(Mutex::new(CurrentLeaderInfo::new(leader_info)));
 
         let receive_txn_thread = Self::receive_txn_thread(
-            tpu_address,
+            cluster_info.clone(),
             receiver,
             leader_info_provider.clone(),
             connection_cache.clone(),
@@ -380,7 +381,7 @@ impl SendTransactionService {
         );
 
         let retry_thread = Self::retry_thread(
-            tpu_address,
+            cluster_info,
             bank_forks.clone(),
             leader_info_provider,
             connection_cache.clone(),
@@ -398,7 +399,7 @@ impl SendTransactionService {
 
     /// Thread responsible for receiving transactions from RPC clients.
     fn receive_txn_thread<T: TpuInfo + std::marker::Send + 'static>(
-        tpu_address: SocketAddr,
+        cluster_info: Arc<ClusterInfo>,
         receiver: Receiver<TransactionInfo>,
         leader_info_provider: Arc<Mutex<CurrentLeaderInfo<T>>>,
         connection_cache: Arc<ConnectionCache>,
@@ -459,6 +460,10 @@ impl SendTransactionService {
                     stats
                         .sent_transactions
                         .fetch_add(transactions.len() as u64, Ordering::Relaxed);
+                    let tpu_address = cluster_info
+                        .my_contact_info()
+                        .tpu(connection_cache.protocol())
+                        .unwrap();
                     Self::send_transactions_in_batch(
                         &tpu_address,
                         &mut transactions,
@@ -505,7 +510,7 @@ impl SendTransactionService {
 
     /// Thread responsible for retrying transactions
     fn retry_thread<T: TpuInfo + std::marker::Send + 'static>(
-        tpu_address: SocketAddr,
+        cluster_info: Arc<ClusterInfo>,
         bank_forks: Arc<RwLock<BankForks>>,
         leader_info_provider: Arc<Mutex<CurrentLeaderInfo<T>>>,
         connection_cache: Arc<ConnectionCache>,
@@ -538,7 +543,10 @@ impl SendTransactionService {
                         let bank_forks = bank_forks.read().unwrap();
                         (bank_forks.root_bank(), bank_forks.working_bank())
                     };
-
+                    let tpu_address = cluster_info
+                        .my_contact_info()
+                        .tpu(connection_cache.protocol())
+                        .unwrap();
                     let _result = Self::process_transactions(
                         &working_bank,
                         &root_bank,
@@ -790,27 +798,40 @@ mod test {
         super::*,
         crate::tpu_info::NullTpuInfo,
         crossbeam_channel::{bounded, unbounded},
+        solana_gossip::contact_info::ContactInfo,
         solana_sdk::{
             account::AccountSharedData,
             genesis_config::create_genesis_config,
             nonce::{self, state::DurableNonce},
             pubkey::Pubkey,
-            signature::Signer,
+            signature::{Keypair, Signer},
             system_program, system_transaction,
+            timing::timestamp,
         },
+        solana_streamer::socket::SocketAddrSpace,
         std::ops::Sub,
     };
 
+    fn new_test_cluster_info() -> Arc<ClusterInfo> {
+        let keypair = Arc::new(Keypair::new());
+        let contact_info = ContactInfo::new_localhost(&keypair.pubkey(), timestamp());
+        Arc::new(ClusterInfo::new(
+            contact_info,
+            keypair,
+            SocketAddrSpace::Unspecified,
+        ))
+    }
+
     #[test]
     fn service_exit() {
-        let tpu_address = "127.0.0.1:0".parse().unwrap();
         let bank = Bank::default_for_tests();
         let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
         let (sender, receiver) = unbounded();
 
         let connection_cache = Arc::new(ConnectionCache::new("connection_cache_test"));
+        let cluster_info = new_test_cluster_info();
         let send_transaction_service = SendTransactionService::new::<NullTpuInfo>(
-            tpu_address,
+            cluster_info,
             &bank_forks,
             None,
             receiver,
@@ -826,7 +847,7 @@ mod test {
 
     #[test]
     fn validator_exit() {
-        let tpu_address = "127.0.0.1:0".parse().unwrap();
+        let cluster_info = new_test_cluster_info();
         let bank = Bank::default_for_tests();
         let bank_forks = Arc::new(RwLock::new(BankForks::new(bank)));
         let (sender, receiver) = bounded(0);
@@ -844,7 +865,7 @@ mod test {
         let exit = Arc::new(AtomicBool::new(false));
         let connection_cache = Arc::new(ConnectionCache::new("connection_cache_test"));
         let _send_transaction_service = SendTransactionService::new::<NullTpuInfo>(
-            tpu_address,
+            cluster_info,
             &bank_forks,
             None,
             receiver,
diff --git a/start b/start
new file mode 100755
index 0000000000..c2f35e272a
--- /dev/null
+++ b/start
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -eu
+
+SOLANA_CONFIG_DIR=./config
+
+mkdir -p $SOLANA_CONFIG_DIR
+NDEBUG=1 ./multinode-demo/setup.sh
+cargo run --release --bin solana-ledger-tool -- -l config/bootstrap-validator/ create-snapshot 0
+NDEBUG=1 ./multinode-demo/faucet.sh
diff --git a/start_multi b/start_multi
new file mode 100755
index 0000000000..66de0032dc
--- /dev/null
+++ b/start_multi
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+set -eu
+
+SOLANA_KEYGEN="cargo run --release --bin solana-keygen --"
+SOLANA_CONFIG_DIR=./config
+
+if [[ ! -d $SOLANA_CONFIG_DIR ]]; then
+  echo "New Config! Generating Identities"
+  mkdir $SOLANA_CONFIG_DIR
+  $SOLANA_KEYGEN new --no-passphrase -so "$SOLANA_CONFIG_DIR"/a/identity.json
+  $SOLANA_KEYGEN new --no-passphrase -so "$SOLANA_CONFIG_DIR"/a/stake-account.json
+  $SOLANA_KEYGEN new --no-passphrase -so "$SOLANA_CONFIG_DIR"/a/vote-account.json
+
+  $SOLANA_KEYGEN new --no-passphrase -so "$SOLANA_CONFIG_DIR"/b/identity.json
+  $SOLANA_KEYGEN new --no-passphrase -so "$SOLANA_CONFIG_DIR"/b/stake-account.json
+  $SOLANA_KEYGEN new --no-passphrase -so "$SOLANA_CONFIG_DIR"/b/vote-account.json
+fi
+
+NDEBUG=1 ./multinode-demo/setup.sh \
+  --bootstrap-validator \
+  "$SOLANA_CONFIG_DIR"/a/identity.json \
+  "$SOLANA_CONFIG_DIR"/a/vote-account.json \
+  "$SOLANA_CONFIG_DIR"/a/stake-account.json \
+  --bootstrap-validator \
+  "$SOLANA_CONFIG_DIR"/b/identity.json \
+  "$SOLANA_CONFIG_DIR"/b/vote-account.json \
+  "$SOLANA_CONFIG_DIR"/b/stake-account.json
+
+cargo run --bin solana-ledger-tool -- -l config/bootstrap-validator/ create-snapshot 0
+NDEBUG=1 ./multinode-demo/faucet.sh
diff --git a/test-validator/src/lib.rs b/test-validator/src/lib.rs
index ef3faca4e9..1e6a3ac9f3 100644
--- a/test-validator/src/lib.rs
+++ b/test-validator/src/lib.rs
@@ -976,6 +976,7 @@ impl TestValidator {
             DEFAULT_TPU_CONNECTION_POOL_SIZE,
             config.tpu_enable_udp,
             config.admin_rpc_service_post_init.clone(),
+            None,
         )?);
 
         // Needed to avoid panics in `solana-responder-gossip` in tests that create a number of
diff --git a/tip-distributor/Cargo.toml b/tip-distributor/Cargo.toml
new file mode 100644
index 0000000000..46f90e61ea
--- /dev/null
+++ b/tip-distributor/Cargo.toml
@@ -0,0 +1,54 @@
+[package]
+name = "solana-tip-distributor"
+version = { workspace = true }
+edition = { workspace = true }
+license = { workspace = true }
+description = "Collection of binaries used to distribute MEV rewards to delegators and validators."
+publish = false
+
+[dependencies]
+anchor-lang = { workspace = true }
+clap = { version = "4.1.11", features = ["derive", "env"] }
+crossbeam-channel = { workspace = true }
+env_logger = { workspace = true }
+futures = { workspace = true }
+gethostname = { workspace = true }
+im = { workspace = true }
+itertools = { workspace = true }
+jito-tip-distribution = { workspace = true }
+jito-tip-payment = { workspace = true }
+log = { workspace = true }
+num-traits = { workspace = true }
+rand = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+solana-client = { workspace = true }
+solana-genesis-utils = { workspace = true }
+solana-ledger = { workspace = true }
+solana-measure = { workspace = true }
+solana-merkle-tree = { workspace = true }
+solana-metrics = { workspace = true }
+solana-program = { workspace = true }
+solana-program-runtime = { workspace = true }
+solana-rpc-client-api = { workspace = true }
+solana-runtime = { workspace = true }
+solana-sdk = { workspace = true }
+solana-stake-program = { workspace = true }
+thiserror = { workspace = true }
+tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
+
+[[bin]]
+name = "solana-stake-meta-generator"
+path = "src/bin/stake-meta-generator.rs"
+
+[[bin]]
+name = "solana-merkle-root-generator"
+path = "src/bin/merkle-root-generator.rs"
+
+[[bin]]
+name = "solana-merkle-root-uploader"
+path = "src/bin/merkle-root-uploader.rs"
+
+[[bin]]
+name = "solana-claim-mev-tips"
+path = "src/bin/claim-mev-tips.rs"
diff --git a/tip-distributor/README.md b/tip-distributor/README.md
new file mode 100644
index 0000000000..fec682879a
--- /dev/null
+++ b/tip-distributor/README.md
@@ -0,0 +1,52 @@
+# Tip Distributor
+This library and collection of binaries are responsible for generating and uploading merkle roots to the on-chain 
+tip-distribution program found [here](https://github.com/jito-foundation/jito-programs/blob/submodule/tip-payment/programs/tip-distribution/src/lib.rs).
+
+## Background
+Each individual validator is assigned a new PDA per epoch where their share of tips, in lamports, will be stored. 
+At the end of the epoch it's expected that validators take a commission and then distribute the rest of the funds
+to their delegators such that delegators receive rewards proportional to their respective delegations. The distribution
+mechanism is via merkle proofs similar to how airdrops work.
+
+The merkle roots are calculated off-chain and uploaded to the validator's **TipDistributionAccount** PDA. Validators may
+elect an account to upload the merkle roots on their behalf. Once uploaded, users can invoke the **claim** instruction
+and receive the rewards they're entitled to. Once all funds are claimed by users the validator can close the account and
+refunded the rent.
+
+## Scripts
+
+### stake-meta-generator
+
+This script generates a JSON file identifying individual stake delegations to a validator, along with amount of lamports 
+in each validator's **TipDistributionAccount**. All validators will be contained in the JSON list, regardless of whether 
+the validator is a participant in the system; participant being indicative of running the jito-solana client to accept tips 
+having initialized a **TipDistributionAccount** PDA account for the epoch.
+
+One edge case that we've taken into account is the last validator in an epoch N receives tips but those tips don't get transferred
+out into the PDA until some slot in epoch N + 1. Due to this we cannot rely on the bank's state at epoch N for lamports amount
+in the PDAs. We use the bank solely to take a snapshot of delegations, but an RPC node to fetch the PDA lamports for more up-to-date data.
+
+### merkle-root-generator
+This script accepts a path to the above JSON file as one of its arguments, and generates a merkle-root into a JSON file.
+
+### merkle-root-uploader
+Uploads the root on-chain.
+
+### claim-mev-tips
+This reads the file outputted by `merkle-root-generator` and finds all eligible accounts to receive mev tips. Transactions
+are created and sent to the RPC server.
+
+
+## How it works?
+In order to use this library as the merkle root creator one must follow the following steps:
+1. Download a ledger snapshot containing the slot of interest, i.e. the last slot in an epoch. The Solana foundation has snapshots that can be found [here](https://console.cloud.google.com/storage/browser/mainnet-beta-ledger-us-ny5).
+2. Download the snapshot onto your worker machine (where this script will run).
+3. Run `solana-ledger-tool -l ${PATH_TO_LEDGER} create-snapshot ${YOUR_SLOT} ${WHERE_TO_CREATE_SNAPSHOT}`
+   1. The snapshot created at `${WHERE_TO_CREATE_SNAPSHOT}` will have the highest slot of `${YOUR_SLOT}`, assuming you downloaded the correct snapshot.
+4. Run `stake-meta-generator --ledger-path ${WHERE_TO_CREATE_SNAPSHOT} --tip-distribution-program-id ${PUBKEY} --out-path ${JSON_OUT_PATH} --snapshot-slot ${SLOT} --rpc-url ${URL}`
+   1. Note: `${WHERE_TO_CREATE_SNAPSHOT}` must be the same in steps 3 & 4.
+5. Run `merkle-root-generator --stake-meta-coll-path ${STAKE_META_COLLECTION_JSON} --rpc-url ${URL} --out-path ${MERKLE_ROOT_PATH}`
+6. Run `merkle-root-uploader --out-path ${MERKLE_ROOT_PATH} --keypair-path ${KEYPAIR_PATH} --rpc-url ${URL} --tip-distribution-program-id ${PROGRAM_ID}`
+7. Run `solana-claim-mev-tips --merkle-trees-path /solana/ledger/autosnapshot/merkle-tree-221615999.json --rpc-url ${URL} --tip-distribution-program-id ${PROGRAM_ID} --keypair-path ${KEYPAIR_PATH}`
+
+Voila!
diff --git a/tip-distributor/src/bin/claim-mev-tips.rs b/tip-distributor/src/bin/claim-mev-tips.rs
new file mode 100644
index 0000000000..e517377c7d
--- /dev/null
+++ b/tip-distributor/src/bin/claim-mev-tips.rs
@@ -0,0 +1,171 @@
+//! This binary claims MEV tips.
+use {
+    clap::Parser,
+    gethostname::gethostname,
+    log::*,
+    solana_metrics::{datapoint_error, datapoint_info, set_host_id},
+    solana_sdk::{pubkey::Pubkey, signature::read_keypair_file},
+    solana_tip_distributor::{
+        claim_mev_workflow::{claim_mev_tips, ClaimMevError},
+        read_json_from_file,
+        reclaim_rent_workflow::reclaim_rent,
+        GeneratedMerkleTreeCollection,
+    },
+    std::{
+        path::PathBuf,
+        sync::Arc,
+        time::{Duration, Instant},
+    },
+};
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+    /// Path to JSON file containing the [GeneratedMerkleTreeCollection] object.
+    #[arg(long, env)]
+    merkle_trees_path: PathBuf,
+
+    /// RPC to send transactions through
+    #[arg(long, env, default_value = "http://localhost:8899")]
+    rpc_url: String,
+
+    /// Tip distribution program ID
+    #[arg(long, env)]
+    tip_distribution_program_id: Pubkey,
+
+    /// Path to keypair
+    #[arg(long, env)]
+    keypair_path: PathBuf,
+
+    /// Number of unique connections to the RPC server for sending txns
+    #[arg(long, env, default_value_t = 128)]
+    rpc_send_connection_count: u64,
+
+    /// Rate-limits the maximum number of GET requests per RPC connection
+    #[arg(long, env, default_value_t = 256)]
+    max_concurrent_rpc_get_reqs: usize,
+
+    /// Number of retries for main claim send loop. Loop is time bounded.
+    #[arg(long, env, default_value_t = 5)]
+    max_loop_retries: u64,
+
+    /// Limits how long before send loop runs before stopping. Defaults to 10 mins
+    #[arg(long, env, default_value_t = 10 * 60)]
+    max_loop_duration_secs: u64,
+
+    /// Specifies whether to reclaim any rent.
+    #[arg(long, env, default_value_t = true)]
+    should_reclaim_rent: bool,
+
+    /// Specifies whether to reclaim rent on behalf of validators from respective TDAs.
+    #[arg(long, env)]
+    should_reclaim_tdas: bool,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), ClaimMevError> {
+    env_logger::init();
+    gethostname()
+        .into_string()
+        .map(set_host_id)
+        .expect("set hostname");
+    let args: Args = Args::parse();
+    let keypair = Arc::new(read_keypair_file(&args.keypair_path).expect("read keypair file"));
+    let merkle_trees: GeneratedMerkleTreeCollection =
+        read_json_from_file(&args.merkle_trees_path).expect("read GeneratedMerkleTreeCollection");
+    let max_loop_duration = Duration::from_secs(args.max_loop_duration_secs);
+
+    info!(
+        "Starting to claim mev tips for epoch: {}",
+        merkle_trees.epoch
+    );
+    let start = Instant::now();
+
+    match claim_mev_tips(
+        merkle_trees.clone(),
+        args.rpc_url.clone(),
+        args.rpc_send_connection_count,
+        args.max_concurrent_rpc_get_reqs,
+        &args.tip_distribution_program_id,
+        keypair.clone(),
+        args.max_loop_retries,
+        max_loop_duration,
+    )
+    .await
+    {
+        Err(e) => {
+            datapoint_error!(
+                "claim_mev_workflow-claim_error",
+                ("epoch", merkle_trees.epoch, i64),
+                ("error", 1, i64),
+                ("err_str", e.to_string(), String),
+                (
+                    "merkle_trees_path",
+                    args.merkle_trees_path.to_string_lossy(),
+                    String
+                ),
+                ("elapsed_us", start.elapsed().as_micros(), i64),
+            );
+            Err(e)
+        }
+        Ok(()) => {
+            datapoint_info!(
+                "claim_mev_workflow-claim_completion",
+                ("epoch", merkle_trees.epoch, i64),
+                (
+                    "merkle_trees_path",
+                    args.merkle_trees_path.to_string_lossy(),
+                    String
+                ),
+                ("elapsed_us", start.elapsed().as_micros(), i64),
+            );
+            Ok(())
+        }
+    }?;
+
+    if args.should_reclaim_rent {
+        let start = Instant::now();
+        match reclaim_rent(
+            args.rpc_url,
+            args.rpc_send_connection_count,
+            args.tip_distribution_program_id,
+            keypair,
+            args.max_loop_retries,
+            max_loop_duration,
+            args.should_reclaim_tdas,
+        )
+        .await
+        {
+            Err(e) => {
+                datapoint_error!(
+                    "claim_mev_workflow-reclaim_rent_error",
+                    ("epoch", merkle_trees.epoch, i64),
+                    ("error", 1, i64),
+                    ("err_str", e.to_string(), String),
+                    (
+                        "merkle_trees_path",
+                        args.merkle_trees_path.to_string_lossy(),
+                        String
+                    ),
+                    ("elapsed_us", start.elapsed().as_micros(), i64),
+                );
+                Err(e)
+            }
+            Ok(()) => {
+                datapoint_info!(
+                    "claim_mev_workflow-reclaim_rent_completion",
+                    ("epoch", merkle_trees.epoch, i64),
+                    (
+                        "merkle_trees_path",
+                        args.merkle_trees_path.to_string_lossy(),
+                        String
+                    ),
+                    ("elapsed_us", start.elapsed().as_micros(), i64),
+                );
+                Ok(())
+            }
+        }?;
+    }
+    solana_metrics::flush(); // sometimes last datapoint doesn't get emitted. this increases likelihood.
+    Ok(())
+}
diff --git a/tip-distributor/src/bin/merkle-root-generator.rs b/tip-distributor/src/bin/merkle-root-generator.rs
new file mode 100644
index 0000000000..9f2d0f9a4e
--- /dev/null
+++ b/tip-distributor/src/bin/merkle-root-generator.rs
@@ -0,0 +1,34 @@
+//! This binary generates a merkle tree for each [TipDistributionAccount]; they are derived
+//! using a user provided [StakeMetaCollection] JSON file.
+
+use {
+    clap::Parser, log::*,
+    solana_tip_distributor::merkle_root_generator_workflow::generate_merkle_root,
+    std::path::PathBuf,
+};
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+    /// Path to JSON file containing the [StakeMetaCollection] object.
+    #[arg(long, env)]
+    stake_meta_coll_path: PathBuf,
+
+    /// RPC to send transactions through. Used to validate what's being claimed is equal to TDA balance minus rent.
+    #[arg(long, env)]
+    rpc_url: String,
+
+    /// Path to JSON file to get populated with tree node data.
+    #[arg(long, env)]
+    out_path: PathBuf,
+}
+
+fn main() {
+    env_logger::init();
+    info!("Starting merkle-root-generator workflow...");
+
+    let args: Args = Args::parse();
+    generate_merkle_root(&args.stake_meta_coll_path, &args.out_path, &args.rpc_url)
+        .expect("merkle tree produced");
+    info!("saved merkle roots to {:?}", args.stake_meta_coll_path);
+}
diff --git a/tip-distributor/src/bin/merkle-root-uploader.rs b/tip-distributor/src/bin/merkle-root-uploader.rs
new file mode 100644
index 0000000000..9000ce66d0
--- /dev/null
+++ b/tip-distributor/src/bin/merkle-root-uploader.rs
@@ -0,0 +1,54 @@
+use {
+    clap::Parser, log::info, solana_sdk::pubkey::Pubkey,
+    solana_tip_distributor::merkle_root_upload_workflow::upload_merkle_root, std::path::PathBuf,
+};
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+    /// Path to JSON file containing the [StakeMetaCollection] object.
+    #[arg(long, env)]
+    merkle_root_path: PathBuf,
+
+    /// The path to the keypair used to sign and pay for the `upload_merkle_root` transactions.
+    #[arg(long, env)]
+    keypair_path: PathBuf,
+
+    /// The RPC to send transactions to.
+    #[arg(long, env)]
+    rpc_url: String,
+
+    /// Tip distribution program ID
+    #[arg(long, env)]
+    tip_distribution_program_id: Pubkey,
+
+    /// Rate-limits the maximum number of requests per RPC connection
+    #[arg(long, env, default_value_t = 100)]
+    max_concurrent_rpc_get_reqs: usize,
+
+    /// Number of transactions to send to RPC at a time.
+    #[arg(long, env, default_value_t = 64)]
+    txn_send_batch_size: usize,
+}
+
+fn main() {
+    env_logger::init();
+
+    let args: Args = Args::parse();
+
+    info!("starting merkle root uploader...");
+    if let Err(e) = upload_merkle_root(
+        &args.merkle_root_path,
+        &args.keypair_path,
+        &args.rpc_url,
+        &args.tip_distribution_program_id,
+        args.max_concurrent_rpc_get_reqs,
+        args.txn_send_batch_size,
+    ) {
+        panic!("failed to upload merkle roots: {:?}", e);
+    }
+    info!(
+        "uploaded merkle roots from file {:?}",
+        args.merkle_root_path
+    );
+}
diff --git a/tip-distributor/src/bin/stake-meta-generator.rs b/tip-distributor/src/bin/stake-meta-generator.rs
new file mode 100644
index 0000000000..be7993be02
--- /dev/null
+++ b/tip-distributor/src/bin/stake-meta-generator.rs
@@ -0,0 +1,67 @@
+//! This binary is responsible for generating a JSON file that contains meta-data about stake
+//! & delegations given a ledger snapshot directory. The JSON file is structured as an array
+//! of [StakeMeta] objects.
+
+use {
+    clap::Parser,
+    log::*,
+    solana_sdk::{clock::Slot, pubkey::Pubkey},
+    solana_tip_distributor::{self, stake_meta_generator_workflow::generate_stake_meta},
+    std::{
+        fs::{self},
+        path::PathBuf,
+        process::exit,
+    },
+};
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+    /// Ledger path, where you created the snapshot.
+    #[arg(long, env, value_parser = Args::ledger_path_parser)]
+    ledger_path: PathBuf,
+
+    /// The tip-distribution program id.
+    #[arg(long, env)]
+    tip_distribution_program_id: Pubkey,
+
+    /// The tip-payment program id.
+    #[arg(long, env)]
+    tip_payment_program_id: Pubkey,
+
+    /// Path to JSON file populated with the [StakeMetaCollection] object.
+    #[arg(long, env)]
+    out_path: String,
+
+    /// The expected snapshot slot.
+    #[arg(long, env)]
+    snapshot_slot: Slot,
+}
+
+impl Args {
+    fn ledger_path_parser(ledger_path: &str) -> Result<PathBuf, &'static str> {
+        Ok(fs::canonicalize(ledger_path).unwrap_or_else(|err| {
+            error!("Unable to access ledger path '{}': {}", ledger_path, err);
+            exit(1);
+        }))
+    }
+}
+
+fn main() {
+    env_logger::init();
+    info!("Starting stake-meta-generator...");
+
+    let args: Args = Args::parse();
+
+    if let Err(e) = generate_stake_meta(
+        &args.ledger_path,
+        &args.snapshot_slot,
+        &args.tip_distribution_program_id,
+        &args.out_path,
+        &args.tip_payment_program_id,
+    ) {
+        error!("error producing stake-meta: {:?}", e);
+    } else {
+        info!("produced stake meta");
+    }
+}
diff --git a/tip-distributor/src/claim_mev_workflow.rs b/tip-distributor/src/claim_mev_workflow.rs
new file mode 100644
index 0000000000..82cdba3827
--- /dev/null
+++ b/tip-distributor/src/claim_mev_workflow.rs
@@ -0,0 +1,448 @@
+use {
+    crate::{
+        claim_mev_workflow::ClaimMevError::{ClaimantNotFound, InsufficientBalance, TDANotFound},
+        minimum_balance, sign_and_send_transactions_with_retries_multi_rpc,
+        GeneratedMerkleTreeCollection, TreeNode,
+    },
+    anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas},
+    itertools::Itertools,
+    jito_tip_distribution::state::{ClaimStatus, Config, TipDistributionAccount},
+    log::{debug, error, info},
+    solana_client::nonblocking::rpc_client::RpcClient,
+    solana_metrics::{datapoint_info, datapoint_warn},
+    solana_program::{
+        fee_calculator::DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, native_token::LAMPORTS_PER_SOL,
+        system_program,
+    },
+    solana_sdk::{
+        account::Account,
+        commitment_config::CommitmentConfig,
+        instruction::Instruction,
+        pubkey::Pubkey,
+        signature::{Keypair, Signer},
+        transaction::Transaction,
+    },
+    std::{
+        collections::HashMap,
+        sync::Arc,
+        time::{Duration, Instant},
+    },
+    thiserror::Error,
+};
+
+#[derive(Error, Debug)]
+pub enum ClaimMevError {
+    #[error(transparent)]
+    IoError(#[from] std::io::Error),
+
+    #[error(transparent)]
+    JsonError(#[from] serde_json::Error),
+
+    #[error(transparent)]
+    AnchorError(anchor_lang::error::Error),
+
+    #[error("TDA not found for pubkey: {0:?}")]
+    TDANotFound(Pubkey),
+
+    #[error("Claim Status not found for pubkey: {0:?}")]
+    ClaimStatusNotFound(Pubkey),
+
+    #[error("Claimant not found for pubkey: {0:?}")]
+    ClaimantNotFound(Pubkey),
+
+    #[error(transparent)]
+    MaxFetchRetriesExceeded(#[from] solana_rpc_client_api::client_error::Error),
+
+    #[error("Failed after {attempts} retries. {remaining_transaction_count} remaining mev claim transactions, {failed_transaction_count} failed requests.",)]
+    MaxSendTransactionRetriesExceeded {
+        attempts: u64,
+        remaining_transaction_count: usize,
+        failed_transaction_count: usize,
+    },
+
+    #[error("Expected to have at least {desired_balance} lamports in {payer:?}. Current balance is {start_balance} lamports. Deposit {sol_to_deposit} SOL to continue.")]
+    InsufficientBalance {
+        desired_balance: u64,
+        payer: Pubkey,
+        start_balance: u64,
+        sol_to_deposit: u64,
+    },
+}
+
+pub async fn claim_mev_tips(
+    merkle_trees: GeneratedMerkleTreeCollection,
+    rpc_url: String,
+    rpc_send_connection_count: u64,
+    max_concurrent_rpc_get_reqs: usize,
+    tip_distribution_program_id: &Pubkey,
+    keypair: Arc<Keypair>,
+    max_loop_retries: u64,
+    max_loop_duration: Duration,
+) -> Result<(), ClaimMevError> {
+    let payer_pubkey = keypair.pubkey();
+    let blockhash_rpc_client = Arc::new(RpcClient::new_with_commitment(
+        rpc_url.clone(),
+        CommitmentConfig::finalized(),
+    ));
+    let rpc_clients = Arc::new(
+        (0..rpc_send_connection_count)
+            .map(|_| {
+                Arc::new(RpcClient::new_with_commitment(
+                    rpc_url.clone(),
+                    CommitmentConfig::confirmed(),
+                ))
+            })
+            .collect_vec(),
+    );
+
+    let tree_nodes = merkle_trees
+        .generated_merkle_trees
+        .iter()
+        .flat_map(|tree| &tree.tree_nodes)
+        .collect_vec();
+
+    // fetch all accounts up front
+    info!(
+        "Starting to fetch accounts for epoch {}",
+        merkle_trees.epoch
+    );
+    let tdas = crate::get_batched_accounts(
+        &blockhash_rpc_client,
+        max_concurrent_rpc_get_reqs,
+        merkle_trees
+            .generated_merkle_trees
+            .iter()
+            .map(|tree| tree.tip_distribution_account)
+            .collect_vec(),
+    )
+    .await
+    .map_err(ClaimMevError::MaxFetchRetriesExceeded)?
+    .into_iter()
+    .filter_map(|(pubkey, maybe_account)| {
+        let Some(account) = maybe_account else {
+            datapoint_warn!(
+                "claim_mev_workflow-account_error",
+                ("epoch", merkle_trees.epoch, i64),
+                ("pubkey", pubkey.to_string(), String),
+                ("account_type", "tip_distribution_account", String),
+                ("error", 1, i64),
+                ("err_type", "fetch", String),
+                ("err_str", "Failed to fetch TipDistributionAccount", String)
+            );
+            return None;
+        };
+
+        let account = match TipDistributionAccount::try_deserialize(&mut account.data.as_slice()) {
+            Ok(a) => a,
+            Err(e) => {
+                datapoint_warn!(
+                    "claim_mev_workflow-account_error",
+                    ("epoch", merkle_trees.epoch, i64),
+                    ("pubkey", pubkey.to_string(), String),
+                    ("account_type", "tip_distribution_account", String),
+                    ("error", 1, i64),
+                    ("err_type", "deserialize_tip_distribution_account", String),
+                    ("err_str", e.to_string(), String)
+                );
+                return None;
+            }
+        };
+        Some((pubkey, account))
+    })
+    .collect::<HashMap<Pubkey, TipDistributionAccount>>();
+
+    // track balances and account len to make sure account is rent-exempt after transfer
+    let claimants = crate::get_batched_accounts(
+        &blockhash_rpc_client,
+        max_concurrent_rpc_get_reqs,
+        tree_nodes
+            .iter()
+            .map(|tree_node| tree_node.claimant)
+            .collect_vec(),
+    )
+    .await
+    .map_err(ClaimMevError::MaxFetchRetriesExceeded)?
+    .into_iter()
+    .map(|(pubkey, maybe_account)| {
+        (
+            pubkey,
+            maybe_account
+                .map(|account| (account.lamports, account.data.len()))
+                .unwrap_or_default(),
+        )
+    })
+    .collect::<HashMap<Pubkey, (u64, usize)>>();
+
+    // Refresh claimants + Try sending txns to RPC
+    let mut retries = 0;
+    let mut failed_transaction_count = 0usize;
+    loop {
+        let start = Instant::now();
+        let claim_statuses = crate::get_batched_accounts(
+            &blockhash_rpc_client,
+            max_concurrent_rpc_get_reqs,
+            tree_nodes
+                .iter()
+                .map(|tree_node| tree_node.claim_status_pubkey)
+                .collect_vec(),
+        )
+        .await
+        .map_err(ClaimMevError::MaxFetchRetriesExceeded)?;
+        let account_fetch_elapsed = start.elapsed();
+
+        let (
+            skipped_merkle_root_count,
+            zero_lamports_count,
+            already_claimed_count,
+            below_min_rent_count,
+            transactions,
+        ) = build_transactions(
+            tip_distribution_program_id,
+            &merkle_trees,
+            &payer_pubkey,
+            &tree_nodes,
+            &tdas,
+            &claimants,
+            &claim_statuses,
+        )?;
+        datapoint_info!(
+            "claim_mev_workflow-prepare_transactions",
+            ("epoch", merkle_trees.epoch, i64),
+            ("attempt", retries, i64),
+            ("tree_node_count", tree_nodes.len(), i64),
+            ("tda_count", tdas.len(), i64),
+            ("claimant_count", claimants.len(), i64),
+            ("claim_status_count", claim_statuses.len(), i64),
+            ("skipped_merkle_root_count", skipped_merkle_root_count, i64),
+            ("zero_lamports_count", zero_lamports_count, i64),
+            ("already_claimed_count", already_claimed_count, i64),
+            ("below_min_rent_count", below_min_rent_count, i64),
+            ("transaction_count", transactions.len(), i64),
+            (
+                "account_fetch_latency_us",
+                account_fetch_elapsed.as_micros(),
+                i64
+            ),
+            (
+                "transaction_prepare_latency_us",
+                start.elapsed().as_micros(),
+                i64
+            ),
+        );
+
+        if transactions.is_empty() {
+            info!("Finished claiming tips after {retries} retries, {failed_transaction_count} failed requests.");
+            return Ok(());
+        }
+
+        if let Some((start_balance, desired_balance, sol_to_deposit)) = is_sufficient_balance(
+            &payer_pubkey,
+            &blockhash_rpc_client,
+            transactions.len() as u64,
+        )
+        .await
+        {
+            return Err(InsufficientBalance {
+                desired_balance,
+                payer: payer_pubkey,
+                start_balance,
+                sol_to_deposit,
+            });
+        }
+        let transactions_len = transactions.len();
+
+        info!("Sending {} tip claim transactions. {zero_lamports_count} would transfer zero lamports, {below_min_rent_count} would be below minimum rent", transactions.len());
+        let send_start = Instant::now();
+        let (remaining_transaction_count, new_failed_transaction_count) =
+            sign_and_send_transactions_with_retries_multi_rpc(
+                &keypair,
+                &blockhash_rpc_client,
+                &rpc_clients,
+                transactions,
+                max_loop_duration,
+            )
+            .await;
+        failed_transaction_count =
+            failed_transaction_count.saturating_add(new_failed_transaction_count);
+
+        datapoint_info!(
+            "claim_mev_workflow-send_transactions",
+            ("epoch", merkle_trees.epoch, i64),
+            ("attempt", retries, i64),
+            ("transaction_count", transactions_len, i64),
+            (
+                "successful_transaction_count",
+                transactions_len.saturating_sub(remaining_transaction_count),
+                i64
+            ),
+            (
+                "remaining_transaction_count",
+                remaining_transaction_count,
+                i64
+            ),
+            (
+                "failed_transaction_count",
+                new_failed_transaction_count,
+                i64
+            ),
+            ("send_latency_us", send_start.elapsed().as_micros(), i64),
+        );
+
+        if retries >= max_loop_retries {
+            return Err(ClaimMevError::MaxSendTransactionRetriesExceeded {
+                attempts: max_loop_retries,
+                remaining_transaction_count,
+                failed_transaction_count,
+            });
+        }
+        retries = retries.saturating_add(1);
+    }
+}
+
+#[allow(clippy::result_large_err)]
+fn build_transactions(
+    tip_distribution_program_id: &Pubkey,
+    merkle_trees: &GeneratedMerkleTreeCollection,
+    payer_pubkey: &Pubkey,
+    tree_nodes: &[&TreeNode],
+    tdas: &HashMap<Pubkey, TipDistributionAccount>,
+    claimants: &HashMap<Pubkey, (u64 /* lamports */, usize /* allocated bytes */)>,
+    claim_statuses: &HashMap<Pubkey, Option<Account>>,
+) -> Result<
+    (
+        usize, /* skipped_merkle_root_count */
+        usize, /* zero_lamports_count */
+        usize, /* already_claimed_count */
+        usize, /* below_min_rent_count */
+        Vec<Transaction>,
+    ),
+    ClaimMevError,
+> {
+    let tip_distribution_config =
+        Pubkey::find_program_address(&[Config::SEED], tip_distribution_program_id).0;
+    let mut skipped_merkle_root_count: usize = 0;
+    let mut zero_lamports_count: usize = 0;
+    let mut already_claimed_count: usize = 0;
+    let mut below_min_rent_count: usize = 0;
+    let mut instructions =
+        Vec::with_capacity(tree_nodes.iter().filter(|node| node.amount > 0).count());
+
+    // prepare instructions to transfer to all claimants
+    for tree in &merkle_trees.generated_merkle_trees {
+        let Some(fetched_tip_distribution_account) = tdas.get(&tree.tip_distribution_account)
+        else {
+            return Err(TDANotFound(tree.tip_distribution_account));
+        };
+        // only claim for ones that have merkle root on-chain
+        if fetched_tip_distribution_account.merkle_root.is_none() {
+            info!(
+                "Merkle root has not uploaded yet. Skipped {} claimants for TDA: {:?}",
+                tree.tree_nodes.len(),
+                tree.tip_distribution_account
+            );
+            skipped_merkle_root_count = skipped_merkle_root_count.checked_add(1).unwrap();
+            continue;
+        }
+        for node in &tree.tree_nodes {
+            if node.amount == 0 {
+                zero_lamports_count = zero_lamports_count.checked_add(1).unwrap();
+                continue;
+            }
+
+            // make sure not previously claimed
+            match claim_statuses.get(&node.claim_status_pubkey) {
+                Some(None) => {} // expected to not find ClaimStatus account, don't skip
+                Some(Some(_account)) => {
+                    debug!(
+                        "Claim status account already exists (already paid out). Skipping pubkey: {:?}.", node.claim_status_pubkey,
+                    );
+                    already_claimed_count = already_claimed_count.checked_add(1).unwrap();
+                    continue;
+                }
+                None => return Err(ClaimantNotFound(node.claim_status_pubkey)),
+            };
+            let Some((current_balance, allocated_bytes)) = claimants.get(&node.claimant) else {
+                return Err(ClaimantNotFound(node.claimant));
+            };
+
+            // some older accounts can be rent-paying
+            // any new transfers will need to make the account rent-exempt (runtime enforced)
+            let new_balance = current_balance.checked_add(node.amount).unwrap();
+            let minimum_rent = minimum_balance(*allocated_bytes);
+            if new_balance < minimum_rent {
+                debug!("Current balance + claim amount of {new_balance} is less than required rent-exempt of {minimum_rent} for pubkey: {}. Skipping.", node.claimant);
+                below_min_rent_count = below_min_rent_count.checked_add(1).unwrap();
+                continue;
+            }
+            instructions.push(Instruction {
+                program_id: *tip_distribution_program_id,
+                data: jito_tip_distribution::instruction::Claim {
+                    proof: node.proof.clone().unwrap(),
+                    amount: node.amount,
+                    bump: node.claim_status_bump,
+                }
+                .data(),
+                accounts: jito_tip_distribution::accounts::Claim {
+                    config: tip_distribution_config,
+                    tip_distribution_account: tree.tip_distribution_account,
+                    claimant: node.claimant,
+                    claim_status: node.claim_status_pubkey,
+                    payer: *payer_pubkey,
+                    system_program: system_program::id(),
+                }
+                .to_account_metas(None),
+            });
+        }
+    }
+
+    let transactions = instructions
+        .into_iter()
+        .map(|ix| Transaction::new_with_payer(&[ix], Some(payer_pubkey)))
+        .collect::<Vec<_>>();
+    Ok((
+        skipped_merkle_root_count,
+        zero_lamports_count,
+        already_claimed_count,
+        below_min_rent_count,
+        transactions,
+    ))
+}
+
+/// heuristic to make sure we have enough funds to cover the rent costs if epoch has many validators
+/// If insufficient funds, returns start balance, desired balance, and amount of sol to deposit
+async fn is_sufficient_balance(
+    payer: &Pubkey,
+    rpc_client: &RpcClient,
+    instruction_count: u64,
+) -> Option<(u64, u64, u64)> {
+    let start_balance = rpc_client
+        .get_balance(payer)
+        .await
+        .expect("Failed to get starting balance");
+    // most amounts are for 0 lamports. had 1736 non-zero claims out of 164742
+    let min_rent_per_claim = rpc_client
+        .get_minimum_balance_for_rent_exemption(ClaimStatus::SIZE)
+        .await
+        .expect("Failed to calculate min rent");
+    let desired_balance = instruction_count
+        .checked_mul(
+            min_rent_per_claim
+                .checked_add(DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE)
+                .unwrap(),
+        )
+        .unwrap();
+    if start_balance < desired_balance {
+        let sol_to_deposit = desired_balance
+            .checked_sub(start_balance)
+            .unwrap()
+            .checked_add(LAMPORTS_PER_SOL)
+            .unwrap()
+            .checked_sub(1)
+            .unwrap()
+            .checked_div(LAMPORTS_PER_SOL)
+            .unwrap(); // rounds up to nearest sol
+        Some((start_balance, desired_balance, sol_to_deposit))
+    } else {
+        None
+    }
+}
diff --git a/tip-distributor/src/lib.rs b/tip-distributor/src/lib.rs
new file mode 100644
index 0000000000..c914adb376
--- /dev/null
+++ b/tip-distributor/src/lib.rs
@@ -0,0 +1,1083 @@
+pub mod claim_mev_workflow;
+pub mod merkle_root_generator_workflow;
+pub mod merkle_root_upload_workflow;
+pub mod reclaim_rent_workflow;
+pub mod stake_meta_generator_workflow;
+
+use {
+    crate::{
+        merkle_root_generator_workflow::MerkleRootGeneratorError,
+        stake_meta_generator_workflow::StakeMetaGeneratorError::CheckedMathError,
+    },
+    anchor_lang::Id,
+    itertools::Itertools,
+    jito_tip_distribution::{
+        program::JitoTipDistribution,
+        state::{ClaimStatus, TipDistributionAccount},
+    },
+    jito_tip_payment::{
+        Config, CONFIG_ACCOUNT_SEED, TIP_ACCOUNT_SEED_0, TIP_ACCOUNT_SEED_1, TIP_ACCOUNT_SEED_2,
+        TIP_ACCOUNT_SEED_3, TIP_ACCOUNT_SEED_4, TIP_ACCOUNT_SEED_5, TIP_ACCOUNT_SEED_6,
+        TIP_ACCOUNT_SEED_7,
+    },
+    log::*,
+    rand::prelude::SliceRandom,
+    serde::{de::DeserializeOwned, Deserialize, Serialize},
+    solana_client::{nonblocking::rpc_client::RpcClient, rpc_client::RpcClient as SyncRpcClient},
+    solana_merkle_tree::MerkleTree,
+    solana_metrics::{datapoint_error, datapoint_warn},
+    solana_program::{
+        instruction::InstructionError,
+        rent::{
+            ACCOUNT_STORAGE_OVERHEAD, DEFAULT_EXEMPTION_THRESHOLD, DEFAULT_LAMPORTS_PER_BYTE_YEAR,
+        },
+    },
+    solana_rpc_client_api::{
+        client_error::{Error, ErrorKind},
+        request::{RpcError, RpcResponseErrorData, MAX_MULTIPLE_ACCOUNTS},
+        response::RpcSimulateTransactionResult,
+    },
+    solana_sdk::{
+        account::{Account, AccountSharedData, ReadableAccount},
+        clock::Slot,
+        hash::{Hash, Hasher},
+        pubkey::Pubkey,
+        signature::{Keypair, Signature},
+        stake_history::Epoch,
+        transaction::{
+            Transaction,
+            TransactionError::{self},
+        },
+    },
+    std::{
+        collections::HashMap,
+        fs::File,
+        io::BufReader,
+        path::PathBuf,
+        sync::{
+            atomic::{AtomicUsize, Ordering},
+            Arc,
+        },
+        time::{Duration, Instant},
+    },
+    tokio::sync::{RwLock, Semaphore},
+};
+
+#[derive(Clone, Deserialize, Serialize, Debug)]
+pub struct GeneratedMerkleTreeCollection {
+    pub generated_merkle_trees: Vec<GeneratedMerkleTree>,
+    pub bank_hash: String,
+    pub epoch: Epoch,
+    pub slot: Slot,
+}
+
+#[derive(Clone, Eq, Debug, Hash, PartialEq, Deserialize, Serialize)]
+pub struct GeneratedMerkleTree {
+    #[serde(with = "pubkey_string_conversion")]
+    pub tip_distribution_account: Pubkey,
+    #[serde(with = "pubkey_string_conversion")]
+    pub merkle_root_upload_authority: Pubkey,
+    pub merkle_root: Hash,
+    pub tree_nodes: Vec<TreeNode>,
+    pub max_total_claim: u64,
+    pub max_num_nodes: u64,
+}
+
+pub struct TipPaymentPubkeys {
+    config_pda: Pubkey,
+    tip_pdas: Vec<Pubkey>,
+}
+
+fn emit_inconsistent_tree_node_amount_dp(
+    tree_nodes: &[TreeNode],
+    tip_distribution_account: &Pubkey,
+    rpc_client: &SyncRpcClient,
+) {
+    let actual_claims: u64 = tree_nodes.iter().map(|t| t.amount).sum();
+    let tda = rpc_client.get_account(tip_distribution_account).unwrap();
+    let min_rent = rpc_client
+        .get_minimum_balance_for_rent_exemption(tda.data.len())
+        .unwrap();
+
+    let expected_claims = tda.lamports.checked_sub(min_rent).unwrap();
+    if actual_claims == expected_claims {
+        return;
+    }
+
+    if actual_claims > expected_claims {
+        datapoint_error!(
+            "tip-distributor",
+            (
+                "actual_claims_exceeded",
+                format!("tip_distribution_account={tip_distribution_account},actual_claims={actual_claims}, expected_claims={expected_claims}"),
+                String
+            ),
+        );
+    } else {
+        datapoint_warn!(
+            "tip-distributor",
+            (
+                "actual_claims_below",
+                format!("tip_distribution_account={tip_distribution_account},actual_claims={actual_claims}, expected_claims={expected_claims}"),
+                String
+            ),
+        );
+    }
+}
+
+impl GeneratedMerkleTreeCollection {
+    pub fn new_from_stake_meta_collection(
+        stake_meta_coll: StakeMetaCollection,
+        maybe_rpc_client: Option<SyncRpcClient>,
+    ) -> Result<GeneratedMerkleTreeCollection, MerkleRootGeneratorError> {
+        let generated_merkle_trees = stake_meta_coll
+            .stake_metas
+            .into_iter()
+            .filter(|stake_meta| stake_meta.maybe_tip_distribution_meta.is_some())
+            .filter_map(|stake_meta| {
+                let mut tree_nodes = match TreeNode::vec_from_stake_meta(&stake_meta) {
+                    Err(e) => return Some(Err(e)),
+                    Ok(maybe_tree_nodes) => maybe_tree_nodes,
+                }?;
+
+                if let Some(rpc_client) = &maybe_rpc_client {
+                    if let Some(tda) = stake_meta.maybe_tip_distribution_meta.as_ref() {
+                        emit_inconsistent_tree_node_amount_dp(
+                            &tree_nodes[..],
+                            &tda.tip_distribution_pubkey,
+                            rpc_client,
+                        );
+                    }
+                }
+
+                let hashed_nodes: Vec<[u8; 32]> =
+                    tree_nodes.iter().map(|n| n.hash().to_bytes()).collect();
+
+                let tip_distribution_meta = stake_meta.maybe_tip_distribution_meta.unwrap();
+
+                let merkle_tree = MerkleTree::new(&hashed_nodes[..], true);
+                let max_num_nodes = tree_nodes.len() as u64;
+
+                for (i, tree_node) in tree_nodes.iter_mut().enumerate() {
+                    tree_node.proof = Some(get_proof(&merkle_tree, i));
+                }
+
+                Some(Ok(GeneratedMerkleTree {
+                    max_num_nodes,
+                    tip_distribution_account: tip_distribution_meta.tip_distribution_pubkey,
+                    merkle_root_upload_authority: tip_distribution_meta
+                        .merkle_root_upload_authority,
+                    merkle_root: *merkle_tree.get_root().unwrap(),
+                    tree_nodes,
+                    max_total_claim: tip_distribution_meta.total_tips,
+                }))
+            })
+            .collect::<Result<Vec<GeneratedMerkleTree>, MerkleRootGeneratorError>>()?;
+
+        Ok(GeneratedMerkleTreeCollection {
+            generated_merkle_trees,
+            bank_hash: stake_meta_coll.bank_hash,
+            epoch: stake_meta_coll.epoch,
+            slot: stake_meta_coll.slot,
+        })
+    }
+}
+
+pub fn get_proof(merkle_tree: &MerkleTree, i: usize) -> Vec<[u8; 32]> {
+    let mut proof = Vec::new();
+    let path = merkle_tree.find_path(i).expect("path to index");
+    for branch in path.get_proof_entries() {
+        if let Some(hash) = branch.get_left_sibling() {
+            proof.push(hash.to_bytes());
+        } else if let Some(hash) = branch.get_right_sibling() {
+            proof.push(hash.to_bytes());
+        } else {
+            panic!("expected some hash at each level of the tree");
+        }
+    }
+    proof
+}
+
+fn derive_tip_payment_pubkeys(program_id: &Pubkey) -> TipPaymentPubkeys {
+    let config_pda = Pubkey::find_program_address(&[CONFIG_ACCOUNT_SEED], program_id).0;
+    let tip_pda_0 = Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_0], program_id).0;
+    let tip_pda_1 = Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_1], program_id).0;
+    let tip_pda_2 = Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_2], program_id).0;
+    let tip_pda_3 = Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_3], program_id).0;
+    let tip_pda_4 = Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_4], program_id).0;
+    let tip_pda_5 = Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_5], program_id).0;
+    let tip_pda_6 = Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_6], program_id).0;
+    let tip_pda_7 = Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_7], program_id).0;
+
+    TipPaymentPubkeys {
+        config_pda,
+        tip_pdas: vec![
+            tip_pda_0, tip_pda_1, tip_pda_2, tip_pda_3, tip_pda_4, tip_pda_5, tip_pda_6, tip_pda_7,
+        ],
+    }
+}
+
+#[derive(Clone, Eq, Debug, Hash, PartialEq, Deserialize, Serialize)]
+pub struct TreeNode {
+    /// The stake account entitled to redeem.
+    #[serde(with = "pubkey_string_conversion")]
+    pub claimant: Pubkey,
+
+    /// Pubkey of the ClaimStatus PDA account, this account should be closed to reclaim rent.
+    #[serde(with = "pubkey_string_conversion")]
+    pub claim_status_pubkey: Pubkey,
+
+    /// Bump of the ClaimStatus PDA account
+    pub claim_status_bump: u8,
+
+    #[serde(with = "pubkey_string_conversion")]
+    pub staker_pubkey: Pubkey,
+
+    #[serde(with = "pubkey_string_conversion")]
+    pub withdrawer_pubkey: Pubkey,
+
+    /// The amount this account is entitled to.
+    pub amount: u64,
+
+    /// The proof associated with this TreeNode
+    pub proof: Option<Vec<[u8; 32]>>,
+}
+
+impl TreeNode {
+    fn vec_from_stake_meta(
+        stake_meta: &StakeMeta,
+    ) -> Result<Option<Vec<TreeNode>>, MerkleRootGeneratorError> {
+        if let Some(tip_distribution_meta) = stake_meta.maybe_tip_distribution_meta.as_ref() {
+            let validator_amount = (tip_distribution_meta.total_tips as u128)
+                .checked_mul(tip_distribution_meta.validator_fee_bps as u128)
+                .unwrap()
+                .checked_div(10_000)
+                .unwrap() as u64;
+            let (claim_status_pubkey, claim_status_bump) = Pubkey::find_program_address(
+                &[
+                    ClaimStatus::SEED,
+                    &stake_meta.validator_vote_account.to_bytes(),
+                    &tip_distribution_meta.tip_distribution_pubkey.to_bytes(),
+                ],
+                &JitoTipDistribution::id(),
+            );
+            let mut tree_nodes = vec![TreeNode {
+                claimant: stake_meta.validator_vote_account,
+                claim_status_pubkey,
+                claim_status_bump,
+                staker_pubkey: Pubkey::default(),
+                withdrawer_pubkey: Pubkey::default(),
+                amount: validator_amount,
+                proof: None,
+            }];
+
+            let remaining_total_rewards = tip_distribution_meta
+                .total_tips
+                .checked_sub(validator_amount)
+                .unwrap() as u128;
+
+            let total_delegated = stake_meta.total_delegated as u128;
+            tree_nodes.extend(
+                stake_meta
+                    .delegations
+                    .iter()
+                    .map(|delegation| {
+                        let amount_delegated = delegation.lamports_delegated as u128;
+                        let reward_amount = (amount_delegated.checked_mul(remaining_total_rewards))
+                            .unwrap()
+                            .checked_div(total_delegated)
+                            .unwrap();
+                        let (claim_status_pubkey, claim_status_bump) = Pubkey::find_program_address(
+                            &[
+                                ClaimStatus::SEED,
+                                &delegation.stake_account_pubkey.to_bytes(),
+                                &tip_distribution_meta.tip_distribution_pubkey.to_bytes(),
+                            ],
+                            &JitoTipDistribution::id(),
+                        );
+                        Ok(TreeNode {
+                            claimant: delegation.stake_account_pubkey,
+                            claim_status_pubkey,
+                            claim_status_bump,
+                            staker_pubkey: delegation.staker_pubkey,
+                            withdrawer_pubkey: delegation.withdrawer_pubkey,
+                            amount: reward_amount as u64,
+                            proof: None,
+                        })
+                    })
+                    .collect::<Result<Vec<TreeNode>, MerkleRootGeneratorError>>()?,
+            );
+
+            Ok(Some(tree_nodes))
+        } else {
+            Ok(None)
+        }
+    }
+
+    fn hash(&self) -> Hash {
+        let mut hasher = Hasher::default();
+        hasher.hash(self.claimant.as_ref());
+        hasher.hash(self.amount.to_le_bytes().as_ref());
+        hasher.result()
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct StakeMetaCollection {
+    /// List of [StakeMeta].
+    pub stake_metas: Vec<StakeMeta>,
+
+    /// base58 encoded tip-distribution program id.
+    #[serde(with = "pubkey_string_conversion")]
+    pub tip_distribution_program_id: Pubkey,
+
+    /// Base58 encoded bank hash this object was generated at.
+    pub bank_hash: String,
+
+    /// Epoch for which this object was generated for.
+    pub epoch: Epoch,
+
+    /// Slot at which this object was generated.
+    pub slot: Slot,
+}
+
+#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
+pub struct StakeMeta {
+    #[serde(with = "pubkey_string_conversion")]
+    pub validator_vote_account: Pubkey,
+
+    #[serde(with = "pubkey_string_conversion")]
+    pub validator_node_pubkey: Pubkey,
+
+    /// The validator's tip-distribution meta if it exists.
+    pub maybe_tip_distribution_meta: Option<TipDistributionMeta>,
+
+    /// Delegations to this validator.
+    pub delegations: Vec<Delegation>,
+
+    /// The total amount of delegations to the validator.
+    pub total_delegated: u64,
+
+    /// The validator's delegation commission rate as a percentage between 0-100.
+    pub commission: u8,
+}
+
+impl Ord for StakeMeta {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.validator_vote_account
+            .cmp(&other.validator_vote_account)
+    }
+}
+
+impl PartialOrd<Self> for StakeMeta {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
+pub struct TipDistributionMeta {
+    #[serde(with = "pubkey_string_conversion")]
+    pub merkle_root_upload_authority: Pubkey,
+
+    #[serde(with = "pubkey_string_conversion")]
+    pub tip_distribution_pubkey: Pubkey,
+
+    /// The validator's total tips in the [TipDistributionAccount].
+    pub total_tips: u64,
+
+    /// The validator's cut of tips from [TipDistributionAccount], calculated from the on-chain
+    /// commission fee bps.
+    pub validator_fee_bps: u16,
+}
+
+impl TipDistributionMeta {
+    fn from_tda_wrapper(
+        tda_wrapper: TipDistributionAccountWrapper,
+        // The amount that will be left remaining in the tda to maintain rent exemption status.
+        rent_exempt_amount: u64,
+    ) -> Result<Self, stake_meta_generator_workflow::StakeMetaGeneratorError> {
+        Ok(TipDistributionMeta {
+            tip_distribution_pubkey: tda_wrapper.tip_distribution_pubkey,
+            total_tips: tda_wrapper
+                .account_data
+                .lamports()
+                .checked_sub(rent_exempt_amount)
+                .ok_or(CheckedMathError)?,
+            validator_fee_bps: tda_wrapper
+                .tip_distribution_account
+                .validator_commission_bps,
+            merkle_root_upload_authority: tda_wrapper
+                .tip_distribution_account
+                .merkle_root_upload_authority,
+        })
+    }
+}
+
+#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
+pub struct Delegation {
+    #[serde(with = "pubkey_string_conversion")]
+    pub stake_account_pubkey: Pubkey,
+
+    #[serde(with = "pubkey_string_conversion")]
+    pub staker_pubkey: Pubkey,
+
+    #[serde(with = "pubkey_string_conversion")]
+    pub withdrawer_pubkey: Pubkey,
+
+    /// Lamports delegated by the stake account
+    pub lamports_delegated: u64,
+}
+
+impl Ord for Delegation {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        (
+            self.stake_account_pubkey,
+            self.withdrawer_pubkey,
+            self.staker_pubkey,
+            self.lamports_delegated,
+        )
+            .cmp(&(
+                other.stake_account_pubkey,
+                other.withdrawer_pubkey,
+                other.staker_pubkey,
+                other.lamports_delegated,
+            ))
+    }
+}
+
+impl PartialOrd<Self> for Delegation {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+/// Convenience wrapper around [TipDistributionAccount]
+pub struct TipDistributionAccountWrapper {
+    pub tip_distribution_account: TipDistributionAccount,
+    pub account_data: AccountSharedData,
+    pub tip_distribution_pubkey: Pubkey,
+}
+
+// TODO: move to program's sdk
+pub fn derive_tip_distribution_account_address(
+    tip_distribution_program_id: &Pubkey,
+    vote_pubkey: &Pubkey,
+    epoch: Epoch,
+) -> (Pubkey, u8) {
+    Pubkey::find_program_address(
+        &[
+            TipDistributionAccount::SEED,
+            vote_pubkey.to_bytes().as_ref(),
+            epoch.to_le_bytes().as_ref(),
+        ],
+        tip_distribution_program_id,
+    )
+}
+
+pub const MAX_RETRIES: usize = 5;
+pub const FAIL_DELAY: Duration = Duration::from_millis(100);
+
+/// Returns unprocessed transactions, along with fail count
+pub async fn sign_and_send_transactions_with_retries_multi_rpc(
+    signer: &Arc<Keypair>,
+    blockhash_rpc_client: &Arc<RpcClient>,
+    rpc_clients: &Arc<Vec<Arc<RpcClient>>>,
+    mut transactions: Vec<Transaction>,
+    max_loop_duration: Duration,
+) -> (
+    usize, /* remaining txn count */
+    usize, /* failed txn count */
+) {
+    let error_count = Arc::new(AtomicUsize::default());
+    let blockhash = Arc::new(RwLock::new(
+        blockhash_rpc_client
+            .get_latest_blockhash()
+            .await
+            .expect("fetch latest blockhash"),
+    ));
+    let transactions_receiver = {
+        let (transactions_sender, transactions_receiver) = crossbeam_channel::unbounded();
+        let mut rng = rand::thread_rng();
+        transactions.shuffle(&mut rng); // shuffle to avoid racing for the same order of txns as other claim-tip processes
+        transactions
+            .into_iter()
+            .for_each(|txn| transactions_sender.send(txn).unwrap());
+        transactions_receiver
+    };
+    let blockhash_refresh_handle = {
+        let blockhash_rpc_client = blockhash_rpc_client.clone();
+        let blockhash = blockhash.clone();
+        let transactions_receiver = transactions_receiver.clone();
+        tokio::spawn(async move {
+            let start = Instant::now();
+            let mut last_blockhash_update = Instant::now();
+            while start.elapsed() < max_loop_duration && !transactions_receiver.is_empty() {
+                // ensure we always have a recent blockhash
+                if last_blockhash_update.elapsed() > Duration::from_secs(2) {
+                    let hash = blockhash_rpc_client
+                        .get_latest_blockhash()
+                        .await
+                        .expect("fetch latest blockhash");
+                    info!(
+                        "Got blockhash {hash:?}. Sending {} transactions to claim mev tips.",
+                        transactions_receiver.len()
+                    );
+                    *blockhash.write().await = hash;
+                    last_blockhash_update = Instant::now();
+                }
+            }
+
+            info!(
+                "Exited blockhash refresh thread. {} transactions remain.",
+                transactions_receiver.len()
+            );
+            transactions_receiver.len()
+        })
+    };
+    let send_handles = rpc_clients
+        .iter()
+        .map(|rpc_client| {
+            let signer = signer.clone();
+            let transactions_receiver = transactions_receiver.clone();
+            let rpc_client = rpc_client.clone();
+            let error_count = error_count.clone();
+            let blockhash = blockhash.clone();
+            tokio::spawn(async move {
+                let mut iterations = 0usize;
+                while let Ok(txn) = transactions_receiver.recv() {
+                    let mut retries = 0usize;
+                    while retries < MAX_RETRIES {
+                        iterations = iterations.saturating_add(1);
+                        let (_signed_txn, res) =
+                            signed_send(&signer, &rpc_client, *blockhash.read().await, txn.clone())
+                                .await;
+                        match res {
+                            Ok(_) => break,
+                            Err(_) => {
+                                retries = retries.saturating_add(1);
+                                error_count.fetch_add(1, Ordering::Relaxed);
+                                tokio::time::sleep(FAIL_DELAY).await;
+                            }
+                        }
+                    }
+                }
+
+                info!("Exited send thread. Ran {iterations} times.");
+            })
+        })
+        .collect_vec();
+
+    for handle in send_handles {
+        if let Err(e) = handle.await {
+            warn!("Error joining handle: {e:?}")
+        }
+    }
+    let remaining_transaction_count = blockhash_refresh_handle.await.unwrap();
+    (
+        remaining_transaction_count,
+        error_count.load(Ordering::Relaxed),
+    )
+}
+
+pub async fn sign_and_send_transactions_with_retries(
+    signer: &Keypair,
+    rpc_client: &RpcClient,
+    max_concurrent_rpc_get_reqs: usize,
+    transactions: Vec<Transaction>,
+    txn_send_batch_size: usize,
+    max_loop_duration: Duration,
+) -> (Vec<Transaction>, HashMap<Signature, Error>) {
+    let semaphore = Arc::new(Semaphore::new(max_concurrent_rpc_get_reqs));
+    let mut errors = HashMap::default();
+    let mut blockhash = rpc_client
+        .get_latest_blockhash()
+        .await
+        .expect("fetch latest blockhash");
+    // track unsigned txns
+    let mut transactions_to_process = transactions
+        .into_iter()
+        .map(|txn| (txn.message_data(), txn))
+        .collect::<HashMap<Vec<u8>, Transaction>>();
+
+    let start = Instant::now();
+    while start.elapsed() < max_loop_duration && !transactions_to_process.is_empty() {
+        // ensure we always have a recent blockhash
+        // blockhashes last max 150 blocks
+        // finalized commitment is ~32 slots behind tip
+        // assuming 0% skip rate (every slot has a block), we’d have roughly 120 slots
+        // or (120*0.4s) = 48s to land a tx before it expires
+        // if we’re refreshing every 30s, then any txs sent immediately before the refresh would likely expire
+        if start.elapsed() > Duration::from_secs(1) {
+            blockhash = rpc_client
+                .get_latest_blockhash()
+                .await
+                .expect("fetch latest blockhash");
+        }
+        info!(
+            "Sending {txn_send_batch_size} of {} transactions to claim mev tips",
+            transactions_to_process.len()
+        );
+        let send_futs = transactions_to_process
+            .iter()
+            .take(txn_send_batch_size)
+            .map(|(hash, txn)| {
+                let semaphore = semaphore.clone();
+                async move {
+                    let _permit = semaphore.acquire_owned().await.unwrap(); // wait until our turn
+                    let (txn, res) = signed_send(signer, rpc_client, blockhash, txn.clone()).await;
+                    (hash.clone(), txn, res)
+                }
+            });
+
+        let send_res = futures::future::join_all(send_futs).await;
+        let new_errors = send_res
+            .into_iter()
+            .filter_map(|(hash, txn, result)| match result {
+                Err(e) => Some((txn.signatures[0], e)),
+                Ok(..) => {
+                    let _ = transactions_to_process.remove(&hash);
+                    None
+                }
+            })
+            .collect::<HashMap<_, _>>();
+
+        errors.extend(new_errors);
+    }
+
+    (transactions_to_process.values().cloned().collect(), errors)
+}
+
+/// Just in time sign and send transaction to RPC
+async fn signed_send(
+    signer: &Keypair,
+    rpc_client: &RpcClient,
+    blockhash: Hash,
+    mut txn: Transaction,
+) -> (Transaction, solana_rpc_client_api::client_error::Result<()>) {
+    txn.sign(&[signer], blockhash); // just in time signing
+    let res = match rpc_client.send_and_confirm_transaction(&txn).await {
+        Ok(_) => Ok(()),
+        Err(e) => {
+            match e.kind {
+                // Already claimed, skip.
+                ErrorKind::TransactionError(TransactionError::AlreadyProcessed)
+                | ErrorKind::TransactionError(TransactionError::InstructionError(
+                    0,
+                    InstructionError::Custom(0),
+                ))
+                | ErrorKind::RpcError(RpcError::RpcResponseError {
+                    data:
+                        RpcResponseErrorData::SendTransactionPreflightFailure(
+                            RpcSimulateTransactionResult {
+                                err:
+                                    Some(TransactionError::InstructionError(
+                                        0,
+                                        InstructionError::Custom(0),
+                                    )),
+                                ..
+                            },
+                        ),
+                    ..
+                }) => Ok(()),
+
+                // transaction got held up too long and blockhash expired. retry txn
+                ErrorKind::TransactionError(TransactionError::BlockhashNotFound) => Err(e),
+
+                // unexpected error, warn and retry
+                _ => {
+                    error!(
+                        "Error sending transaction. Signature: {}, Error: {e:?}",
+                        txn.signatures[0]
+                    );
+                    Err(e)
+                }
+            }
+        }
+    };
+
+    (txn, res)
+}
+
+/// Fetch accounts in parallel batches with retries.
+async fn get_batched_accounts(
+    rpc_client: &RpcClient,
+    max_concurrent_rpc_get_reqs: usize,
+    pubkeys: Vec<Pubkey>,
+) -> solana_rpc_client_api::client_error::Result<HashMap<Pubkey, Option<Account>>> {
+    let semaphore = Arc::new(Semaphore::new(max_concurrent_rpc_get_reqs));
+    let futs = pubkeys.chunks(MAX_MULTIPLE_ACCOUNTS).map(|pubkeys| {
+        let semaphore = semaphore.clone();
+
+        async move {
+            let _permit = semaphore.acquire_owned().await.unwrap(); // wait until our turn
+            let mut retries = 0usize;
+            loop {
+                match rpc_client.get_multiple_accounts(pubkeys).await {
+                    Ok(accts) => return Ok(accts),
+                    Err(e) => {
+                        retries = retries.saturating_add(1);
+                        if retries == MAX_RETRIES {
+                            datapoint_error!(
+                                "claim_mev_workflow-get_batched_accounts_error",
+                                ("pubkeys", format!("{pubkeys:?}"), String),
+                                ("error", 1, i64),
+                                ("err_type", "fetch_account", String),
+                                ("err_str", e.to_string(), String)
+                            );
+                            return Err(e);
+                        }
+                        tokio::time::sleep(FAIL_DELAY).await;
+                    }
+                }
+            }
+        }
+    });
+
+    let claimant_accounts = futures::future::join_all(futs)
+        .await
+        .into_iter()
+        .collect::<solana_rpc_client_api::client_error::Result<Vec<Vec<Option<Account>>>>>()? // fail on single error
+        .into_iter()
+        .flatten()
+        .collect_vec();
+
+    Ok(pubkeys.into_iter().zip(claimant_accounts).collect())
+}
+
+/// Calculates the minimum balance needed to be rent-exempt
+/// taken from: https://github.com/jito-foundation/jito-solana/blob/d1ba42180d0093dd59480a77132477323a8e3f88/sdk/program/src/rent.rs#L78
+pub fn minimum_balance(data_len: usize) -> u64 {
+    ((((ACCOUNT_STORAGE_OVERHEAD
+        .checked_add(data_len as u64)
+        .unwrap())
+    .checked_mul(DEFAULT_LAMPORTS_PER_BYTE_YEAR))
+    .unwrap() as f64)
+        * DEFAULT_EXEMPTION_THRESHOLD) as u64
+}
+
+mod pubkey_string_conversion {
+    use {
+        serde::{self, Deserialize, Deserializer, Serializer},
+        solana_sdk::pubkey::Pubkey,
+        std::str::FromStr,
+    };
+
+    pub(crate) fn serialize<S>(pubkey: &Pubkey, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        serializer.serialize_str(&pubkey.to_string())
+    }
+
+    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Pubkey, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        Pubkey::from_str(&s).map_err(serde::de::Error::custom)
+    }
+}
+
+pub fn read_json_from_file<T>(path: &PathBuf) -> serde_json::Result<T>
+where
+    T: DeserializeOwned,
+{
+    let file = File::open(path).unwrap();
+    let reader = BufReader::new(file);
+    serde_json::from_reader(reader)
+}
+
+#[cfg(test)]
+mod tests {
+    use {super::*, jito_tip_distribution::merkle_proof};
+
+    #[test]
+    fn test_merkle_tree_verify() {
+        // Create the merkle tree and proofs
+        let tda = Pubkey::new_unique();
+        let (acct_0, acct_1) = (Pubkey::new_unique(), Pubkey::new_unique());
+        let claim_statuses = &[(acct_0, tda), (acct_1, tda)]
+            .iter()
+            .map(|(claimant, tda)| {
+                Pubkey::find_program_address(
+                    &[ClaimStatus::SEED, &claimant.to_bytes(), &tda.to_bytes()],
+                    &JitoTipDistribution::id(),
+                )
+            })
+            .collect::<Vec<(Pubkey, u8)>>();
+        let tree_nodes = vec![
+            TreeNode {
+                claimant: acct_0,
+                claim_status_pubkey: claim_statuses[0].0,
+                claim_status_bump: claim_statuses[0].1,
+                staker_pubkey: Pubkey::default(),
+                withdrawer_pubkey: Pubkey::default(),
+                amount: 151_507,
+                proof: None,
+            },
+            TreeNode {
+                claimant: acct_1,
+                claim_status_pubkey: claim_statuses[1].0,
+                claim_status_bump: claim_statuses[1].1,
+                staker_pubkey: Pubkey::default(),
+                withdrawer_pubkey: Pubkey::default(),
+                amount: 176_624,
+                proof: None,
+            },
+        ];
+
+        // First the nodes are hashed and merkle tree constructed
+        let hashed_nodes: Vec<[u8; 32]> = tree_nodes.iter().map(|n| n.hash().to_bytes()).collect();
+        let mk = MerkleTree::new(&hashed_nodes[..], true);
+        let root = mk.get_root().expect("to have valid root").to_bytes();
+
+        // verify first node
+        let node = solana_program::hash::hashv(&[&[0u8], &hashed_nodes[0]]);
+        let proof = get_proof(&mk, 0);
+        assert!(merkle_proof::verify(proof, root, node.to_bytes()));
+
+        // verify second node
+        let node = solana_program::hash::hashv(&[&[0u8], &hashed_nodes[1]]);
+        let proof = get_proof(&mk, 1);
+        assert!(merkle_proof::verify(proof, root, node.to_bytes()));
+    }
+
+    #[test]
+    fn test_new_from_stake_meta_collection_happy_path() {
+        let merkle_root_upload_authority = Pubkey::new_unique();
+
+        let (tda_0, tda_1) = (Pubkey::new_unique(), Pubkey::new_unique());
+
+        let stake_account_0 = Pubkey::new_unique();
+        let stake_account_1 = Pubkey::new_unique();
+        let stake_account_2 = Pubkey::new_unique();
+        let stake_account_3 = Pubkey::new_unique();
+
+        let staker_account_0 = Pubkey::new_unique();
+        let staker_account_1 = Pubkey::new_unique();
+        let staker_account_2 = Pubkey::new_unique();
+        let staker_account_3 = Pubkey::new_unique();
+
+        let validator_vote_account_0 = Pubkey::new_unique();
+        let validator_vote_account_1 = Pubkey::new_unique();
+
+        let validator_id_0 = Pubkey::new_unique();
+        let validator_id_1 = Pubkey::new_unique();
+
+        let stake_meta_collection = StakeMetaCollection {
+            stake_metas: vec![
+                StakeMeta {
+                    validator_vote_account: validator_vote_account_0,
+                    validator_node_pubkey: validator_id_0,
+                    maybe_tip_distribution_meta: Some(TipDistributionMeta {
+                        merkle_root_upload_authority,
+                        tip_distribution_pubkey: tda_0,
+                        total_tips: 1_900_122_111_000,
+                        validator_fee_bps: 100,
+                    }),
+                    delegations: vec![
+                        Delegation {
+                            stake_account_pubkey: stake_account_0,
+                            staker_pubkey: staker_account_0,
+                            withdrawer_pubkey: staker_account_0,
+                            lamports_delegated: 123_999_123_555,
+                        },
+                        Delegation {
+                            stake_account_pubkey: stake_account_1,
+                            staker_pubkey: staker_account_1,
+                            withdrawer_pubkey: staker_account_1,
+                            lamports_delegated: 144_555_444_556,
+                        },
+                    ],
+                    total_delegated: 1_555_123_000_333_454_000,
+                    commission: 100,
+                },
+                StakeMeta {
+                    validator_vote_account: validator_vote_account_1,
+                    validator_node_pubkey: validator_id_1,
+                    maybe_tip_distribution_meta: Some(TipDistributionMeta {
+                        merkle_root_upload_authority,
+                        tip_distribution_pubkey: tda_1,
+                        total_tips: 1_900_122_111_333,
+                        validator_fee_bps: 200,
+                    }),
+                    delegations: vec![
+                        Delegation {
+                            stake_account_pubkey: stake_account_2,
+                            staker_pubkey: staker_account_2,
+                            withdrawer_pubkey: staker_account_2,
+                            lamports_delegated: 224_555_444,
+                        },
+                        Delegation {
+                            stake_account_pubkey: stake_account_3,
+                            staker_pubkey: staker_account_3,
+                            withdrawer_pubkey: staker_account_3,
+                            lamports_delegated: 700_888_944_555,
+                        },
+                    ],
+                    total_delegated: 2_565_318_909_444_123,
+                    commission: 10,
+                },
+            ],
+            tip_distribution_program_id: Pubkey::new_unique(),
+            bank_hash: Hash::new_unique().to_string(),
+            epoch: 100,
+            slot: 2_000_000,
+        };
+
+        let merkle_tree_collection = GeneratedMerkleTreeCollection::new_from_stake_meta_collection(
+            stake_meta_collection.clone(),
+            None,
+        )
+        .unwrap();
+
+        assert_eq!(stake_meta_collection.epoch, merkle_tree_collection.epoch);
+        assert_eq!(
+            stake_meta_collection.bank_hash,
+            merkle_tree_collection.bank_hash
+        );
+        assert_eq!(stake_meta_collection.slot, merkle_tree_collection.slot);
+        assert_eq!(
+            stake_meta_collection.stake_metas.len(),
+            merkle_tree_collection.generated_merkle_trees.len()
+        );
+        let claim_statuses = &[
+            (validator_vote_account_0, tda_0),
+            (stake_account_0, tda_0),
+            (stake_account_1, tda_0),
+            (validator_vote_account_1, tda_1),
+            (stake_account_2, tda_1),
+            (stake_account_3, tda_1),
+        ]
+        .iter()
+        .map(|(claimant, tda)| {
+            Pubkey::find_program_address(
+                &[ClaimStatus::SEED, &claimant.to_bytes(), &tda.to_bytes()],
+                &JitoTipDistribution::id(),
+            )
+        })
+        .collect::<Vec<(Pubkey, u8)>>();
+        let tree_nodes = vec![
+            TreeNode {
+                claimant: validator_vote_account_0,
+                claim_status_pubkey: claim_statuses[0].0,
+                claim_status_bump: claim_statuses[0].1,
+                staker_pubkey: Pubkey::default(),
+                withdrawer_pubkey: Pubkey::default(),
+                amount: 19_001_221_110,
+                proof: None,
+            },
+            TreeNode {
+                claimant: stake_account_0,
+                claim_status_pubkey: claim_statuses[1].0,
+                claim_status_bump: claim_statuses[1].1,
+                staker_pubkey: Pubkey::default(),
+                withdrawer_pubkey: Pubkey::default(),
+                amount: 149_992,
+                proof: None,
+            },
+            TreeNode {
+                claimant: stake_account_1,
+                claim_status_pubkey: claim_statuses[2].0,
+                claim_status_bump: claim_statuses[2].1,
+                staker_pubkey: Pubkey::default(),
+                withdrawer_pubkey: Pubkey::default(),
+                amount: 174_858,
+                proof: None,
+            },
+        ];
+        let hashed_nodes: Vec<[u8; 32]> = tree_nodes.iter().map(|n| n.hash().to_bytes()).collect();
+        let merkle_tree = MerkleTree::new(&hashed_nodes[..], true);
+        let gmt_0 = GeneratedMerkleTree {
+            tip_distribution_account: tda_0,
+            merkle_root_upload_authority,
+            merkle_root: *merkle_tree.get_root().unwrap(),
+            tree_nodes,
+            max_total_claim: stake_meta_collection.stake_metas[0]
+                .clone()
+                .maybe_tip_distribution_meta
+                .unwrap()
+                .total_tips,
+            max_num_nodes: 3,
+        };
+
+        let tree_nodes = vec![
+            TreeNode {
+                claimant: validator_vote_account_1,
+                claim_status_pubkey: claim_statuses[3].0,
+                claim_status_bump: claim_statuses[3].1,
+                staker_pubkey: Pubkey::default(),
+                withdrawer_pubkey: Pubkey::default(),
+                amount: 38_002_442_226,
+                proof: None,
+            },
+            TreeNode {
+                claimant: stake_account_2,
+                claim_status_pubkey: claim_statuses[4].0,
+                claim_status_bump: claim_statuses[4].1,
+                staker_pubkey: Pubkey::default(),
+                withdrawer_pubkey: Pubkey::default(),
+                amount: 163_000,
+                proof: None,
+            },
+            TreeNode {
+                claimant: stake_account_3,
+                claim_status_pubkey: claim_statuses[5].0,
+                claim_status_bump: claim_statuses[5].1,
+                staker_pubkey: Pubkey::default(),
+                withdrawer_pubkey: Pubkey::default(),
+                amount: 508_762_900,
+                proof: None,
+            },
+        ];
+        let hashed_nodes: Vec<[u8; 32]> = tree_nodes.iter().map(|n| n.hash().to_bytes()).collect();
+        let merkle_tree = MerkleTree::new(&hashed_nodes[..], true);
+        let gmt_1 = GeneratedMerkleTree {
+            tip_distribution_account: tda_1,
+            merkle_root_upload_authority,
+            merkle_root: *merkle_tree.get_root().unwrap(),
+            tree_nodes,
+            max_total_claim: stake_meta_collection.stake_metas[1]
+                .clone()
+                .maybe_tip_distribution_meta
+                .unwrap()
+                .total_tips,
+            max_num_nodes: 3,
+        };
+
+        let expected_generated_merkle_trees = vec![gmt_0, gmt_1];
+        let actual_generated_merkle_trees = merkle_tree_collection.generated_merkle_trees;
+
+        expected_generated_merkle_trees
+            .iter()
+            .for_each(|expected_gmt| {
+                let actual_gmt = actual_generated_merkle_trees
+                    .iter()
+                    .find(|gmt| {
+                        gmt.tip_distribution_account == expected_gmt.tip_distribution_account
+                    })
+                    .unwrap();
+
+                assert_eq!(expected_gmt.max_num_nodes, actual_gmt.max_num_nodes);
+                assert_eq!(expected_gmt.max_total_claim, actual_gmt.max_total_claim);
+                assert_eq!(
+                    expected_gmt.tip_distribution_account,
+                    actual_gmt.tip_distribution_account
+                );
+                assert_eq!(expected_gmt.tree_nodes.len(), actual_gmt.tree_nodes.len());
+                expected_gmt
+                    .tree_nodes
+                    .iter()
+                    .for_each(|expected_tree_node| {
+                        let actual_tree_node = actual_gmt
+                            .tree_nodes
+                            .iter()
+                            .find(|tree_node| tree_node.claimant == expected_tree_node.claimant)
+                            .unwrap();
+                        assert_eq!(expected_tree_node.amount, actual_tree_node.amount);
+                    });
+                assert_eq!(expected_gmt.merkle_root, actual_gmt.merkle_root);
+            });
+    }
+}
diff --git a/tip-distributor/src/merkle_root_generator_workflow.rs b/tip-distributor/src/merkle_root_generator_workflow.rs
new file mode 100644
index 0000000000..bee3da016b
--- /dev/null
+++ b/tip-distributor/src/merkle_root_generator_workflow.rs
@@ -0,0 +1,54 @@
+use {
+    crate::{read_json_from_file, GeneratedMerkleTreeCollection, StakeMetaCollection},
+    log::*,
+    solana_client::rpc_client::RpcClient,
+    std::{
+        fmt::Debug,
+        fs::File,
+        io::{BufWriter, Write},
+        path::PathBuf,
+    },
+    thiserror::Error,
+};
+
+#[derive(Error, Debug)]
+pub enum MerkleRootGeneratorError {
+    #[error(transparent)]
+    IoError(#[from] std::io::Error),
+
+    #[error(transparent)]
+    RpcError(#[from] Box<solana_client::client_error::ClientError>),
+
+    #[error(transparent)]
+    SerdeJsonError(#[from] serde_json::Error),
+}
+
+pub fn generate_merkle_root(
+    stake_meta_coll_path: &PathBuf,
+    out_path: &PathBuf,
+    rpc_url: &str,
+) -> Result<(), MerkleRootGeneratorError> {
+    let stake_meta_coll: StakeMetaCollection = read_json_from_file(stake_meta_coll_path)?;
+
+    let rpc_client = RpcClient::new(rpc_url);
+    let merkle_tree_coll = GeneratedMerkleTreeCollection::new_from_stake_meta_collection(
+        stake_meta_coll,
+        Some(rpc_client),
+    )?;
+
+    write_to_json_file(&merkle_tree_coll, out_path)?;
+    Ok(())
+}
+
+fn write_to_json_file(
+    merkle_tree_coll: &GeneratedMerkleTreeCollection,
+    file_path: &PathBuf,
+) -> Result<(), MerkleRootGeneratorError> {
+    let file = File::create(file_path)?;
+    let mut writer = BufWriter::new(file);
+    let json = serde_json::to_string_pretty(&merkle_tree_coll).unwrap();
+    writer.write_all(json.as_bytes())?;
+    writer.flush()?;
+
+    Ok(())
+}
diff --git a/tip-distributor/src/merkle_root_upload_workflow.rs b/tip-distributor/src/merkle_root_upload_workflow.rs
new file mode 100644
index 0000000000..e40465581f
--- /dev/null
+++ b/tip-distributor/src/merkle_root_upload_workflow.rs
@@ -0,0 +1,138 @@
+use {
+    crate::{
+        read_json_from_file, sign_and_send_transactions_with_retries, GeneratedMerkleTree,
+        GeneratedMerkleTreeCollection,
+    },
+    anchor_lang::AccountDeserialize,
+    jito_tip_distribution::{
+        sdk::instruction::{upload_merkle_root_ix, UploadMerkleRootAccounts, UploadMerkleRootArgs},
+        state::{Config, TipDistributionAccount},
+    },
+    log::{error, info},
+    solana_client::nonblocking::rpc_client::RpcClient,
+    solana_program::{
+        fee_calculator::DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, native_token::LAMPORTS_PER_SOL,
+    },
+    solana_sdk::{
+        commitment_config::CommitmentConfig,
+        pubkey::Pubkey,
+        signature::{read_keypair_file, Signer},
+        transaction::Transaction,
+    },
+    std::{path::PathBuf, time::Duration},
+    thiserror::Error,
+    tokio::runtime::Builder,
+};
+
+#[derive(Error, Debug)]
+pub enum MerkleRootUploadError {
+    #[error(transparent)]
+    IoError(#[from] std::io::Error),
+
+    #[error(transparent)]
+    JsonError(#[from] serde_json::Error),
+}
+
+pub fn upload_merkle_root(
+    merkle_root_path: &PathBuf,
+    keypair_path: &PathBuf,
+    rpc_url: &str,
+    tip_distribution_program_id: &Pubkey,
+    max_concurrent_rpc_get_reqs: usize,
+    txn_send_batch_size: usize,
+) -> Result<(), MerkleRootUploadError> {
+    const MAX_RETRY_DURATION: Duration = Duration::from_secs(600);
+
+    let merkle_tree: GeneratedMerkleTreeCollection =
+        read_json_from_file(merkle_root_path).expect("read GeneratedMerkleTreeCollection");
+    let keypair = read_keypair_file(keypair_path).expect("read keypair file");
+
+    let tip_distribution_config =
+        Pubkey::find_program_address(&[Config::SEED], tip_distribution_program_id).0;
+
+    let runtime = Builder::new_multi_thread()
+        .worker_threads(16)
+        .enable_all()
+        .build()
+        .expect("build runtime");
+
+    runtime.block_on(async move {
+        let rpc_client =
+            RpcClient::new_with_commitment(rpc_url.to_string(), CommitmentConfig::confirmed());
+        let trees: Vec<GeneratedMerkleTree> = merkle_tree
+            .generated_merkle_trees
+            .into_iter()
+            .filter(|tree| tree.merkle_root_upload_authority == keypair.pubkey())
+            .collect();
+
+        info!("num trees to upload: {:?}", trees.len());
+
+        // heuristic to make sure we have enough funds to cover execution, assumes all trees need updating 
+        {
+            let initial_balance = rpc_client.get_balance(&keypair.pubkey()).await.expect("failed to get balance");
+            let desired_balance = (trees.len() as u64).checked_mul(DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE).unwrap();
+            if initial_balance < desired_balance {
+                let sol_to_deposit = desired_balance.checked_sub(initial_balance).unwrap().checked_add(LAMPORTS_PER_SOL).unwrap().checked_sub(1).unwrap().checked_div(LAMPORTS_PER_SOL).unwrap(); // rounds up to nearest sol
+                panic!("Expected to have at least {} lamports in {}, current balance is {} lamports, deposit {} SOL to continue.",
+                       desired_balance, &keypair.pubkey(), initial_balance, sol_to_deposit)
+            }
+        }
+        let mut trees_needing_update: Vec<GeneratedMerkleTree> = vec![];
+        for tree in trees {
+            let account = rpc_client
+                .get_account(&tree.tip_distribution_account)
+                .await
+                .expect("fetch expect");
+
+            let mut data = account.data.as_slice();
+            let fetched_tip_distribution_account =
+                TipDistributionAccount::try_deserialize(&mut data)
+                    .expect("failed to deserialize tip_distribution_account state");
+
+            let needs_upload = match fetched_tip_distribution_account.merkle_root {
+                Some(merkle_root) => {
+                    merkle_root.total_funds_claimed == 0
+                        && merkle_root.root != tree.merkle_root.to_bytes()
+                }
+                None => true,
+            };
+
+            if needs_upload {
+                trees_needing_update.push(tree);
+            }
+        }
+
+        info!("num trees need uploading: {:?}", trees_needing_update.len());
+
+        let transactions: Vec<Transaction> = trees_needing_update
+            .iter()
+            .map(|tree| {
+                let ix = upload_merkle_root_ix(
+                    *tip_distribution_program_id,
+                    UploadMerkleRootArgs {
+                        root: tree.merkle_root.to_bytes(),
+                        max_total_claim: tree.max_total_claim,
+                        max_num_nodes: tree.max_num_nodes,
+                    },
+                    UploadMerkleRootAccounts {
+                        config: tip_distribution_config,
+                        merkle_root_upload_authority: keypair.pubkey(),
+                        tip_distribution_account: tree.tip_distribution_account,
+                    },
+                );
+                Transaction::new_with_payer(
+                    &[ix],
+                    Some(&keypair.pubkey()),
+                )
+            })
+            .collect();
+
+        let (to_process, failed_transactions) = sign_and_send_transactions_with_retries(
+            &keypair, &rpc_client, max_concurrent_rpc_get_reqs, transactions, txn_send_batch_size, MAX_RETRY_DURATION).await;
+        if !to_process.is_empty() {
+            panic!("{} remaining mev claim transactions, {} failed requests.", to_process.len(), failed_transactions.len());
+        }
+    });
+
+    Ok(())
+}
diff --git a/tip-distributor/src/reclaim_rent_workflow.rs b/tip-distributor/src/reclaim_rent_workflow.rs
new file mode 100644
index 0000000000..8d923e3980
--- /dev/null
+++ b/tip-distributor/src/reclaim_rent_workflow.rs
@@ -0,0 +1,247 @@
+use {
+    crate::{
+        claim_mev_workflow::ClaimMevError, reclaim_rent_workflow::ClaimMevError::AnchorError,
+        sign_and_send_transactions_with_retries_multi_rpc,
+    },
+    anchor_lang::AccountDeserialize,
+    itertools::Itertools,
+    jito_tip_distribution::{
+        sdk::{
+            derive_config_account_address,
+            instruction::{
+                close_claim_status_ix, close_tip_distribution_account_ix, CloseClaimStatusAccounts,
+                CloseClaimStatusArgs, CloseTipDistributionAccountArgs,
+                CloseTipDistributionAccounts,
+            },
+        },
+        state::{ClaimStatus, Config, TipDistributionAccount},
+    },
+    log::info,
+    solana_client::nonblocking::rpc_client::RpcClient,
+    solana_measure::measure,
+    solana_metrics::datapoint_info,
+    solana_program::pubkey::Pubkey,
+    solana_sdk::{
+        commitment_config::CommitmentConfig,
+        signature::{Keypair, Signer},
+        transaction::Transaction,
+    },
+    std::{
+        sync::Arc,
+        time::{Duration, Instant},
+    },
+};
+
+/// Clear old ClaimStatus accounts
+pub async fn reclaim_rent(
+    rpc_url: String,
+    rpc_send_connection_count: u64,
+    tip_distribution_program_id: Pubkey,
+    signer: Arc<Keypair>,
+    max_loop_retries: u64,
+    max_loop_duration: Duration,
+    // Optionally reclaim TipDistributionAccount rents on behalf of validators.
+    should_reclaim_tdas: bool,
+) -> Result<(), ClaimMevError> {
+    let blockhash_rpc_client = Arc::new(RpcClient::new_with_timeout_and_commitment(
+        rpc_url.clone(),
+        Duration::from_secs(180), // 3 mins
+        CommitmentConfig::finalized(),
+    ));
+    let rpc_clients = Arc::new(
+        (0..rpc_send_connection_count)
+            .map(|_| {
+                Arc::new(RpcClient::new_with_commitment(
+                    rpc_url.clone(),
+                    CommitmentConfig::confirmed(),
+                ))
+            })
+            .collect_vec(),
+    );
+    let mut retries = 0;
+    let mut failed_transaction_count = 0usize;
+    let signer_pubkey = signer.pubkey();
+    loop {
+        let (transactions, get_pa_elapsed, transaction_prepare_elaspsed) = build_transactions(
+            blockhash_rpc_client.clone(),
+            &tip_distribution_program_id,
+            &signer_pubkey,
+            should_reclaim_tdas,
+        )
+        .await?;
+        datapoint_info!(
+            "claim_mev_workflow-prepare_rent_reclaim_transactions",
+            ("attempt", retries, i64),
+            ("transaction_count", transactions.len(), i64),
+            ("account_fetch_latency_us", get_pa_elapsed.as_micros(), i64),
+            (
+                "transaction_prepare_latency_us",
+                transaction_prepare_elaspsed.as_micros(),
+                i64
+            ),
+        );
+        let transactions_len = transactions.len();
+        if transactions.is_empty() {
+            info!("Finished reclaim rent after {retries} retries, {failed_transaction_count} failed requests.");
+            return Ok(());
+        }
+
+        info!("Sending {} rent reclaim transactions", transactions.len());
+        let send_start = Instant::now();
+        let (remaining_transaction_count, new_failed_transaction_count) =
+            sign_and_send_transactions_with_retries_multi_rpc(
+                &signer,
+                &blockhash_rpc_client,
+                &rpc_clients,
+                transactions,
+                max_loop_duration,
+            )
+            .await;
+        failed_transaction_count =
+            failed_transaction_count.saturating_add(new_failed_transaction_count);
+
+        datapoint_info!(
+            "claim_mev_workflow-send_reclaim_rent_transactions",
+            ("attempt", retries, i64),
+            ("transaction_count", transactions_len, i64),
+            (
+                "successful_transaction_count",
+                transactions_len.saturating_sub(remaining_transaction_count),
+                i64
+            ),
+            (
+                "remaining_transaction_count",
+                remaining_transaction_count,
+                i64
+            ),
+            (
+                "failed_transaction_count",
+                new_failed_transaction_count,
+                i64
+            ),
+            ("send_latency_us", send_start.elapsed().as_micros(), i64),
+        );
+
+        if retries >= max_loop_retries {
+            return Err(ClaimMevError::MaxSendTransactionRetriesExceeded {
+                attempts: max_loop_retries,
+                remaining_transaction_count,
+                failed_transaction_count,
+            });
+        }
+        retries = retries.saturating_add(1);
+    }
+}
+
+async fn build_transactions(
+    rpc_client: Arc<RpcClient>,
+    tip_distribution_program_id: &Pubkey,
+    signer_pubkey: &Pubkey,
+    should_reclaim_tdas: bool,
+) -> Result<(Vec<Transaction>, Duration, Duration), ClaimMevError> {
+    info!("Fetching program accounts");
+    let (accounts, get_pa_elapsed) = measure!(
+        rpc_client
+            .get_program_accounts(tip_distribution_program_id)
+            .await?
+    );
+    info!(
+        "Fetch get_program_accounts took {:?} and fetched {} accounts",
+        get_pa_elapsed.as_duration(),
+        accounts.len()
+    );
+
+    info!("Fetching current_epoch");
+    let current_epoch = rpc_client.get_epoch_info().await?.epoch;
+    info!("Fetch current_epoch: {current_epoch}");
+
+    info!("Fetching Config account");
+    let config_pubkey = derive_config_account_address(tip_distribution_program_id).0;
+    let (config_account, elapsed) = measure!(rpc_client.get_account(&config_pubkey).await?);
+    info!("Fetch Config account took {:?}", elapsed.as_duration());
+    let config_account: Config =
+        Config::try_deserialize(&mut config_account.data.as_slice()).map_err(AnchorError)?;
+
+    info!("Filtering for ClaimStatus accounts");
+    let claim_status_accounts: Vec<(Pubkey, ClaimStatus)> = accounts
+        .iter()
+        .filter_map(|(pubkey, account)| {
+            let claim_status = ClaimStatus::try_deserialize(&mut account.data.as_slice()).ok()?;
+            Some((*pubkey, claim_status))
+        })
+        .filter(|(_, claim_status): &(Pubkey, ClaimStatus)| {
+            // Only return claim statuses that we've paid for and ones that are expired to avoid transaction failures.
+            claim_status.claim_status_payer.eq(signer_pubkey)
+                && current_epoch > claim_status.expires_at
+        })
+        .collect::<Vec<_>>();
+    info!(
+        "{} ClaimStatus accounts eligible for rent reclaim",
+        claim_status_accounts.len()
+    );
+
+    info!("Creating CloseClaimStatusAccounts transactions");
+    let transaction_now = Instant::now();
+    let mut transactions = claim_status_accounts
+        .into_iter()
+        .map(|(claim_status_pubkey, claim_status)| {
+            close_claim_status_ix(
+                *tip_distribution_program_id,
+                CloseClaimStatusArgs,
+                CloseClaimStatusAccounts {
+                    config: config_pubkey,
+                    claim_status: claim_status_pubkey,
+                    claim_status_payer: claim_status.claim_status_payer,
+                },
+            )
+        })
+        .collect::<Vec<_>>()
+        .chunks(4)
+        .map(|instructions| Transaction::new_with_payer(instructions, Some(signer_pubkey)))
+        .collect::<Vec<_>>();
+
+    info!(
+        "Create CloseClaimStatusAccounts transactions took {:?}",
+        transaction_now.elapsed()
+    );
+
+    if should_reclaim_tdas {
+        info!("Creating CloseTipDistributionAccounts transactions");
+        let now = Instant::now();
+        let close_tda_txs = accounts
+            .into_iter()
+            .filter_map(|(pubkey, account)| {
+                let tda =
+                    TipDistributionAccount::try_deserialize(&mut account.data.as_slice()).ok()?;
+                Some((pubkey, tda))
+            })
+            .filter(|(_, tda): &(Pubkey, TipDistributionAccount)| current_epoch > tda.expires_at)
+            .map(|(tip_distribution_account, tda)| {
+                close_tip_distribution_account_ix(
+                    *tip_distribution_program_id,
+                    CloseTipDistributionAccountArgs {
+                        _epoch: tda.epoch_created_at,
+                    },
+                    CloseTipDistributionAccounts {
+                        config: config_pubkey,
+                        tip_distribution_account,
+                        validator_vote_account: tda.validator_vote_account,
+                        expired_funds_account: config_account.expired_funds_account,
+                        signer: *signer_pubkey,
+                    },
+                )
+            })
+            .collect::<Vec<_>>()
+            .chunks(4)
+            .map(|instructions| Transaction::new_with_payer(instructions, Some(signer_pubkey)))
+            .collect::<Vec<_>>();
+        info!("Create CloseTipDistributionAccounts transactions took {:?}, closing {} tip distribution accounts", now.elapsed(), close_tda_txs.len());
+
+        transactions.extend(close_tda_txs);
+    }
+    Ok((
+        transactions,
+        get_pa_elapsed.as_duration(),
+        transaction_now.elapsed(),
+    ))
+}
diff --git a/tip-distributor/src/stake_meta_generator_workflow.rs b/tip-distributor/src/stake_meta_generator_workflow.rs
new file mode 100644
index 0000000000..b35e7929f7
--- /dev/null
+++ b/tip-distributor/src/stake_meta_generator_workflow.rs
@@ -0,0 +1,952 @@
+use {
+    crate::{
+        derive_tip_distribution_account_address, derive_tip_payment_pubkeys, Config, StakeMeta,
+        StakeMetaCollection, TipDistributionAccount, TipDistributionAccountWrapper,
+        TipDistributionMeta,
+    },
+    anchor_lang::AccountDeserialize,
+    itertools::Itertools,
+    log::*,
+    solana_client::client_error::ClientError,
+    solana_ledger::{
+        bank_forks_utils,
+        blockstore::BlockstoreError,
+        blockstore_processor::{BlockstoreProcessorError, ProcessOptions},
+    },
+    solana_runtime::{
+        bank::Bank,
+        hardened_unpack::{open_genesis_config, MAX_GENESIS_ARCHIVE_UNPACKED_SIZE},
+        snapshot_config::SnapshotConfig,
+        stakes::StakeAccount,
+        vote_account::VoteAccount,
+    },
+    solana_sdk::{
+        account::{ReadableAccount, WritableAccount},
+        clock::Slot,
+        pubkey::Pubkey,
+    },
+    std::{
+        collections::HashMap,
+        fmt::{Debug, Display, Formatter},
+        fs::File,
+        io::{BufWriter, Write},
+        mem::size_of,
+        path::{Path, PathBuf},
+        sync::{atomic::AtomicBool, Arc},
+    },
+    thiserror::Error,
+};
+
+#[derive(Error, Debug)]
+pub enum StakeMetaGeneratorError {
+    #[error(transparent)]
+    AnchorError(#[from] Box<anchor_lang::error::Error>),
+
+    #[error(transparent)]
+    BlockstoreError(#[from] BlockstoreError),
+
+    #[error(transparent)]
+    BlockstoreProcessorError(#[from] BlockstoreProcessorError),
+
+    #[error(transparent)]
+    IoError(#[from] std::io::Error),
+
+    CheckedMathError,
+
+    #[error(transparent)]
+    RpcError(#[from] ClientError),
+
+    #[error(transparent)]
+    SerdeJsonError(#[from] serde_json::Error),
+
+    SnapshotSlotNotFound,
+}
+
+impl Display for StakeMetaGeneratorError {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        Debug::fmt(&self, f)
+    }
+}
+
+/// Runs the entire workflow of creating a bank from a snapshot to writing stake meta-data
+/// to a JSON file.
+pub fn generate_stake_meta(
+    ledger_path: &Path,
+    snapshot_slot: &Slot,
+    tip_distribution_program_id: &Pubkey,
+    out_path: &str,
+    tip_payment_program_id: &Pubkey,
+) -> Result<(), StakeMetaGeneratorError> {
+    info!("Creating bank from ledger path...");
+    let bank = create_bank_from_snapshot(ledger_path, snapshot_slot)?;
+
+    info!("Generating stake_meta_collection object...");
+    let stake_meta_coll =
+        generate_stake_meta_collection(&bank, tip_distribution_program_id, tip_payment_program_id)?;
+
+    info!("Writing stake_meta_collection to JSON {}...", out_path);
+    write_to_json_file(&stake_meta_coll, out_path)?;
+
+    Ok(())
+}
+
+fn create_bank_from_snapshot(
+    ledger_path: &Path,
+    snapshot_slot: &Slot,
+) -> Result<Arc<Bank>, StakeMetaGeneratorError> {
+    let genesis_config = open_genesis_config(ledger_path, MAX_GENESIS_ARCHIVE_UNPACKED_SIZE);
+    let snapshot_config = SnapshotConfig {
+        full_snapshot_archive_interval_slots: Slot::MAX,
+        incremental_snapshot_archive_interval_slots: Slot::MAX,
+        full_snapshot_archives_dir: PathBuf::from(ledger_path),
+        incremental_snapshot_archives_dir: PathBuf::from(ledger_path),
+        bank_snapshots_dir: PathBuf::from(ledger_path),
+        ..SnapshotConfig::default()
+    };
+    let (bank_forks, _snapshot_hashes) = bank_forks_utils::bank_forks_from_snapshot(
+        &genesis_config,
+        vec![PathBuf::from(ledger_path).join(Path::new("stake-meta.accounts"))],
+        None,
+        &snapshot_config,
+        &ProcessOptions::default(),
+        None,
+        &Arc::new(AtomicBool::new(false)),
+    );
+
+    let working_bank = bank_forks.read().unwrap().working_bank();
+    assert_eq!(
+        working_bank.slot(),
+        *snapshot_slot,
+        "expected working bank slot {}, found {}",
+        snapshot_slot,
+        working_bank.slot()
+    );
+
+    Ok(working_bank)
+}
+
+fn write_to_json_file(
+    stake_meta_coll: &StakeMetaCollection,
+    out_path: &str,
+) -> Result<(), StakeMetaGeneratorError> {
+    let file = File::create(out_path)?;
+    let mut writer = BufWriter::new(file);
+    let json = serde_json::to_string_pretty(&stake_meta_coll).unwrap();
+    writer.write_all(json.as_bytes())?;
+    writer.flush()?;
+
+    Ok(())
+}
+
+/// Creates a collection of [StakeMeta]'s from the given bank.
+pub fn generate_stake_meta_collection(
+    bank: &Arc<Bank>,
+    tip_distribution_program_id: &Pubkey,
+    tip_payment_program_id: &Pubkey,
+) -> Result<StakeMetaCollection, StakeMetaGeneratorError> {
+    assert!(bank.is_frozen());
+
+    let epoch_vote_accounts = bank.epoch_vote_accounts(bank.epoch()).unwrap_or_else(|| {
+        panic!(
+            "No epoch_vote_accounts found for slot {} at epoch {}",
+            bank.slot(),
+            bank.epoch()
+        )
+    });
+
+    let l_stakes = bank.stakes_cache.stakes();
+    let delegations = l_stakes.stake_delegations();
+
+    let voter_pubkey_to_delegations = group_delegations_by_voter_pubkey(delegations, bank);
+
+    // the last leader in an epoch may not crank the tip program before the epoch is over, which
+    // would result in MEV rewards for epoch N not being cranked until epoch N + 1. This means that
+    // the account balance in the snapshot could be incorrect.
+    // We assume that the rewards sitting in the tip program PDAs are cranked out by the time all of
+    // the rewards are claimed.
+    let tip_accounts = derive_tip_payment_pubkeys(tip_payment_program_id);
+    let account = bank
+        .get_account(&tip_accounts.config_pda)
+        .expect("config pda exists");
+
+    let config = Config::try_deserialize(&mut account.data()).expect("deserializes configuration");
+
+    let bb_commission_pct: u64 = config.block_builder_commission_pct;
+    let tip_receiver: Pubkey = config.tip_receiver;
+
+    // includes the block builder fee
+    let excess_tip_balances: u64 = tip_accounts
+        .tip_pdas
+        .iter()
+        .map(|pubkey| {
+            let tip_account = bank.get_account(pubkey).expect("tip account exists");
+            tip_account
+                .lamports()
+                .checked_sub(bank.get_minimum_balance_for_rent_exemption(tip_account.data().len()))
+                .expect("tip balance underflow")
+        })
+        .sum();
+    // matches math in tip payment program
+    let block_builder_tips = excess_tip_balances
+        .checked_mul(bb_commission_pct)
+        .expect("block_builder_tips overflow")
+        .checked_div(100)
+        .expect("block_builder_tips division error");
+    let tip_receiver_fee = excess_tip_balances
+        .checked_sub(block_builder_tips)
+        .expect("tip_receiver_fee doesnt underflow");
+
+    let vote_pk_and_maybe_tdas: Vec<(
+        (Pubkey, &VoteAccount),
+        Option<TipDistributionAccountWrapper>,
+    )> = epoch_vote_accounts
+        .iter()
+        .map(|(vote_pubkey, (_total_stake, vote_account))| {
+            let tip_distribution_pubkey = derive_tip_distribution_account_address(
+                tip_distribution_program_id,
+                vote_pubkey,
+                bank.epoch(),
+            )
+            .0;
+            let tda = if let Some(mut account_data) = bank.get_account(&tip_distribution_pubkey) {
+                // TDAs may be funded with lamports and therefore exist in the bank, but would fail the deserialization step
+                // if the buffer is yet to be allocated thru the init call to the program.
+                if let Ok(tip_distribution_account) =
+                    TipDistributionAccount::try_deserialize(&mut account_data.data())
+                {
+                    // this snapshot might have tips that weren't claimed by the time the epoch is over
+                    // assume that it will eventually be cranked and credit the excess to this account
+                    if tip_distribution_pubkey == tip_receiver {
+                        account_data.set_lamports(
+                            account_data
+                                .lamports()
+                                .checked_add(tip_receiver_fee)
+                                .expect("tip overflow"),
+                        );
+                    }
+                    Some(TipDistributionAccountWrapper {
+                        tip_distribution_account,
+                        account_data,
+                        tip_distribution_pubkey,
+                    })
+                } else {
+                    None
+                }
+            } else {
+                None
+            };
+            Ok(((*vote_pubkey, vote_account), tda))
+        })
+        .collect::<Result<_, StakeMetaGeneratorError>>()?;
+
+    let mut stake_metas = vec![];
+    for ((vote_pubkey, vote_account), maybe_tda) in vote_pk_and_maybe_tdas {
+        if let Some(mut delegations) = voter_pubkey_to_delegations.get(&vote_pubkey).cloned() {
+            let total_delegated = delegations.iter().fold(0u64, |sum, delegation| {
+                sum.checked_add(delegation.lamports_delegated).unwrap()
+            });
+
+            let maybe_tip_distribution_meta = if let Some(tda) = maybe_tda {
+                let actual_len = tda.account_data.data().len();
+                let expected_len = 8_usize.saturating_add(size_of::<TipDistributionAccount>());
+                if actual_len != expected_len {
+                    warn!("len mismatch actual={actual_len}, expected={expected_len}");
+                }
+                let rent_exempt_amount =
+                    bank.get_minimum_balance_for_rent_exemption(tda.account_data.data().len());
+
+                Some(TipDistributionMeta::from_tda_wrapper(
+                    tda,
+                    rent_exempt_amount,
+                )?)
+            } else {
+                None
+            };
+
+            let vote_state = vote_account.vote_state().unwrap();
+            delegations.sort();
+            stake_metas.push(StakeMeta {
+                maybe_tip_distribution_meta,
+                validator_node_pubkey: vote_state.node_pubkey,
+                validator_vote_account: vote_pubkey,
+                delegations,
+                total_delegated,
+                commission: vote_state.commission,
+            });
+        } else {
+            warn!(
+                    "voter_pubkey not found in voter_pubkey_to_delegations map [validator_vote_pubkey={}]",
+                    vote_pubkey
+                );
+        }
+    }
+    stake_metas.sort();
+
+    Ok(StakeMetaCollection {
+        stake_metas,
+        tip_distribution_program_id: *tip_distribution_program_id,
+        bank_hash: bank.hash().to_string(),
+        epoch: bank.epoch(),
+        slot: bank.slot(),
+    })
+}
+
+/// Given an [EpochStakes] object, return delegations grouped by voter_pubkey (validator delegated to).
+fn group_delegations_by_voter_pubkey(
+    delegations: &im::HashMap<Pubkey, StakeAccount>,
+    bank: &Bank,
+) -> HashMap<Pubkey, Vec<crate::Delegation>> {
+    delegations
+        .into_iter()
+        .filter(|(_stake_pubkey, stake_account)| {
+            stake_account.delegation().stake(bank.epoch(), None, None) > 0
+        })
+        .into_group_map_by(|(_stake_pubkey, stake_account)| stake_account.delegation().voter_pubkey)
+        .into_iter()
+        .map(|(voter_pubkey, group)| {
+            (
+                voter_pubkey,
+                group
+                    .into_iter()
+                    .map(|(stake_pubkey, stake_account)| crate::Delegation {
+                        stake_account_pubkey: *stake_pubkey,
+                        staker_pubkey: stake_account
+                            .stake_state()
+                            .authorized()
+                            .map(|a| a.staker)
+                            .unwrap_or_default(),
+                        withdrawer_pubkey: stake_account
+                            .stake_state()
+                            .authorized()
+                            .map(|a| a.withdrawer)
+                            .unwrap_or_default(),
+                        lamports_delegated: stake_account.delegation().stake,
+                    })
+                    .collect::<Vec<crate::Delegation>>(),
+            )
+        })
+        .collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use {
+        super::*,
+        crate::derive_tip_distribution_account_address,
+        anchor_lang::AccountSerialize,
+        jito_tip_distribution::state::TipDistributionAccount,
+        jito_tip_payment::{
+            InitBumps, TipPaymentAccount, CONFIG_ACCOUNT_SEED, TIP_ACCOUNT_SEED_0,
+            TIP_ACCOUNT_SEED_1, TIP_ACCOUNT_SEED_2, TIP_ACCOUNT_SEED_3, TIP_ACCOUNT_SEED_4,
+            TIP_ACCOUNT_SEED_5, TIP_ACCOUNT_SEED_6, TIP_ACCOUNT_SEED_7,
+        },
+        solana_runtime::genesis_utils::{
+            create_genesis_config_with_vote_accounts, GenesisConfigInfo, ValidatorVoteKeypairs,
+        },
+        solana_sdk::{
+            self,
+            account::{from_account, AccountSharedData},
+            message::Message,
+            signature::{Keypair, Signer},
+            stake::{
+                self,
+                state::{Authorized, Lockup},
+            },
+            stake_history::StakeHistory,
+            sysvar,
+            transaction::Transaction,
+        },
+        solana_stake_program::stake_state,
+    };
+
+    #[test]
+    fn test_generate_stake_meta_collection_happy_path() {
+        /* 1. Create a Bank seeded with some validator stake accounts */
+        let validator_keypairs_0 = ValidatorVoteKeypairs::new_rand();
+        let validator_keypairs_1 = ValidatorVoteKeypairs::new_rand();
+        let validator_keypairs_2 = ValidatorVoteKeypairs::new_rand();
+        let validator_keypairs = vec![
+            &validator_keypairs_0,
+            &validator_keypairs_1,
+            &validator_keypairs_2,
+        ];
+        const INITIAL_VALIDATOR_STAKES: u64 = 10_000;
+        let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts(
+            1_000_000_000,
+            &validator_keypairs,
+            vec![INITIAL_VALIDATOR_STAKES; 3],
+        );
+
+        let bank = Bank::new_for_tests(&genesis_config);
+
+        /* 2. Seed the Bank with [TipDistributionAccount]'s */
+        let merkle_root_upload_authority = Pubkey::new_unique();
+        let tip_distribution_program_id = Pubkey::new_unique();
+        let tip_payment_program_id = Pubkey::new_unique();
+
+        let delegator_0 = Keypair::new();
+        let delegator_1 = Keypair::new();
+        let delegator_2 = Keypair::new();
+        let delegator_3 = Keypair::new();
+        let delegator_4 = Keypair::new();
+
+        let delegator_0_pk = delegator_0.pubkey();
+        let delegator_1_pk = delegator_1.pubkey();
+        let delegator_2_pk = delegator_2.pubkey();
+        let delegator_3_pk = delegator_3.pubkey();
+        let delegator_4_pk = delegator_4.pubkey();
+
+        let d_0_data = AccountSharedData::new(
+            300_000_000_000_000 * 10,
+            0,
+            &solana_sdk::system_program::id(),
+        );
+        let d_1_data = AccountSharedData::new(
+            100_000_203_000_000 * 10,
+            0,
+            &solana_sdk::system_program::id(),
+        );
+        let d_2_data = AccountSharedData::new(
+            100_000_235_899_000 * 10,
+            0,
+            &solana_sdk::system_program::id(),
+        );
+        let d_3_data = AccountSharedData::new(
+            200_000_000_000_000 * 10,
+            0,
+            &solana_sdk::system_program::id(),
+        );
+        let d_4_data = AccountSharedData::new(
+            100_000_000_777_000 * 10,
+            0,
+            &solana_sdk::system_program::id(),
+        );
+
+        bank.store_account(&delegator_0_pk, &d_0_data);
+        bank.store_account(&delegator_1_pk, &d_1_data);
+        bank.store_account(&delegator_2_pk, &d_2_data);
+        bank.store_account(&delegator_3_pk, &d_3_data);
+        bank.store_account(&delegator_4_pk, &d_4_data);
+
+        /* 3. Delegate some stake to the initial set of validators */
+        let mut validator_0_delegations = vec![crate::Delegation {
+            stake_account_pubkey: validator_keypairs_0.stake_keypair.pubkey(),
+            staker_pubkey: validator_keypairs_0.stake_keypair.pubkey(),
+            withdrawer_pubkey: validator_keypairs_0.stake_keypair.pubkey(),
+            lamports_delegated: INITIAL_VALIDATOR_STAKES,
+        }];
+        let stake_account = delegate_stake_helper(
+            &bank,
+            &delegator_0,
+            &validator_keypairs_0.vote_keypair.pubkey(),
+            30_000_000_000,
+        );
+        validator_0_delegations.push(crate::Delegation {
+            stake_account_pubkey: stake_account,
+            staker_pubkey: delegator_0.pubkey(),
+            withdrawer_pubkey: delegator_0.pubkey(),
+            lamports_delegated: 30_000_000_000,
+        });
+        let stake_account = delegate_stake_helper(
+            &bank,
+            &delegator_1,
+            &validator_keypairs_0.vote_keypair.pubkey(),
+            3_000_000_000,
+        );
+        validator_0_delegations.push(crate::Delegation {
+            stake_account_pubkey: stake_account,
+            staker_pubkey: delegator_1.pubkey(),
+            withdrawer_pubkey: delegator_1.pubkey(),
+            lamports_delegated: 3_000_000_000,
+        });
+        let stake_account = delegate_stake_helper(
+            &bank,
+            &delegator_2,
+            &validator_keypairs_0.vote_keypair.pubkey(),
+            33_000_000_000,
+        );
+        validator_0_delegations.push(crate::Delegation {
+            stake_account_pubkey: stake_account,
+            staker_pubkey: delegator_2.pubkey(),
+            withdrawer_pubkey: delegator_2.pubkey(),
+            lamports_delegated: 33_000_000_000,
+        });
+
+        let mut validator_1_delegations = vec![crate::Delegation {
+            stake_account_pubkey: validator_keypairs_1.stake_keypair.pubkey(),
+            staker_pubkey: validator_keypairs_1.stake_keypair.pubkey(),
+            withdrawer_pubkey: validator_keypairs_1.stake_keypair.pubkey(),
+            lamports_delegated: INITIAL_VALIDATOR_STAKES,
+        }];
+        let stake_account = delegate_stake_helper(
+            &bank,
+            &delegator_3,
+            &validator_keypairs_1.vote_keypair.pubkey(),
+            4_222_364_000,
+        );
+        validator_1_delegations.push(crate::Delegation {
+            stake_account_pubkey: stake_account,
+            staker_pubkey: delegator_3.pubkey(),
+            withdrawer_pubkey: delegator_3.pubkey(),
+            lamports_delegated: 4_222_364_000,
+        });
+        let stake_account = delegate_stake_helper(
+            &bank,
+            &delegator_4,
+            &validator_keypairs_1.vote_keypair.pubkey(),
+            6_000_000_527,
+        );
+        validator_1_delegations.push(crate::Delegation {
+            stake_account_pubkey: stake_account,
+            staker_pubkey: delegator_4.pubkey(),
+            withdrawer_pubkey: delegator_4.pubkey(),
+            lamports_delegated: 6_000_000_527,
+        });
+
+        let mut validator_2_delegations = vec![crate::Delegation {
+            stake_account_pubkey: validator_keypairs_2.stake_keypair.pubkey(),
+            staker_pubkey: validator_keypairs_2.stake_keypair.pubkey(),
+            withdrawer_pubkey: validator_keypairs_2.stake_keypair.pubkey(),
+            lamports_delegated: INITIAL_VALIDATOR_STAKES,
+        }];
+        let stake_account = delegate_stake_helper(
+            &bank,
+            &delegator_0,
+            &validator_keypairs_2.vote_keypair.pubkey(),
+            1_300_123_156,
+        );
+        validator_2_delegations.push(crate::Delegation {
+            stake_account_pubkey: stake_account,
+            staker_pubkey: delegator_0.pubkey(),
+            withdrawer_pubkey: delegator_0.pubkey(),
+            lamports_delegated: 1_300_123_156,
+        });
+        let stake_account = delegate_stake_helper(
+            &bank,
+            &delegator_4,
+            &validator_keypairs_2.vote_keypair.pubkey(),
+            1_610_565_420,
+        );
+        validator_2_delegations.push(crate::Delegation {
+            stake_account_pubkey: stake_account,
+            staker_pubkey: delegator_4.pubkey(),
+            withdrawer_pubkey: delegator_4.pubkey(),
+            lamports_delegated: 1_610_565_420,
+        });
+
+        /* 4. Run assertions */
+        fn warmed_up(bank: &Bank, stake_pubkeys: &[Pubkey]) -> bool {
+            for stake_pubkey in stake_pubkeys {
+                let stake =
+                    stake_state::stake_from(&bank.get_account(stake_pubkey).unwrap()).unwrap();
+
+                if stake.delegation.stake
+                    != stake.stake(
+                        bank.epoch(),
+                        Some(
+                            &from_account::<StakeHistory, _>(
+                                &bank.get_account(&sysvar::stake_history::id()).unwrap(),
+                            )
+                            .unwrap(),
+                        ),
+                        None,
+                    )
+                {
+                    return false;
+                }
+            }
+
+            true
+        }
+        fn next_epoch(bank: &Arc<Bank>) -> Arc<Bank> {
+            bank.squash();
+
+            Arc::new(Bank::new_from_parent(
+                bank,
+                &Pubkey::default(),
+                bank.get_slots_in_epoch(bank.epoch()) + bank.slot(),
+            ))
+        }
+
+        let mut bank = Arc::new(bank);
+        let mut stake_pubkeys = validator_0_delegations
+            .iter()
+            .map(|v| v.stake_account_pubkey)
+            .collect::<Vec<Pubkey>>();
+        stake_pubkeys.extend(
+            validator_1_delegations
+                .iter()
+                .map(|v| v.stake_account_pubkey),
+        );
+        stake_pubkeys.extend(
+            validator_2_delegations
+                .iter()
+                .map(|v| v.stake_account_pubkey),
+        );
+        loop {
+            if warmed_up(&bank, &stake_pubkeys[..]) {
+                break;
+            }
+
+            // Cycle thru banks until we're fully warmed up
+            bank = next_epoch(&bank);
+        }
+
+        let tip_distribution_account_0 = derive_tip_distribution_account_address(
+            &tip_distribution_program_id,
+            &validator_keypairs_0.vote_keypair.pubkey(),
+            bank.epoch(),
+        );
+        let tip_distribution_account_1 = derive_tip_distribution_account_address(
+            &tip_distribution_program_id,
+            &validator_keypairs_1.vote_keypair.pubkey(),
+            bank.epoch(),
+        );
+        let tip_distribution_account_2 = derive_tip_distribution_account_address(
+            &tip_distribution_program_id,
+            &validator_keypairs_2.vote_keypair.pubkey(),
+            bank.epoch(),
+        );
+
+        let expires_at = bank.epoch() + 3;
+
+        let tda_0 = TipDistributionAccount {
+            validator_vote_account: validator_keypairs_0.vote_keypair.pubkey(),
+            merkle_root_upload_authority,
+            merkle_root: None,
+            epoch_created_at: bank.epoch(),
+            validator_commission_bps: 50,
+            expires_at,
+            bump: tip_distribution_account_0.1,
+        };
+        let tda_1 = TipDistributionAccount {
+            validator_vote_account: validator_keypairs_1.vote_keypair.pubkey(),
+            merkle_root_upload_authority,
+            merkle_root: None,
+            epoch_created_at: bank.epoch(),
+            validator_commission_bps: 500,
+            expires_at: 0,
+            bump: tip_distribution_account_1.1,
+        };
+        let tda_2 = TipDistributionAccount {
+            validator_vote_account: validator_keypairs_2.vote_keypair.pubkey(),
+            merkle_root_upload_authority,
+            merkle_root: None,
+            epoch_created_at: bank.epoch(),
+            validator_commission_bps: 75,
+            expires_at: 0,
+            bump: tip_distribution_account_2.1,
+        };
+
+        let tip_distro_0_tips = 1_000_000 * 10;
+        let tip_distro_1_tips = 69_000_420 * 10;
+        let tip_distro_2_tips = 789_000_111 * 10;
+
+        let tda_0_fields = (tip_distribution_account_0.0, tda_0.validator_commission_bps);
+        let data_0 =
+            tda_to_account_shared_data(&tip_distribution_program_id, tip_distro_0_tips, tda_0);
+        let tda_1_fields = (tip_distribution_account_1.0, tda_1.validator_commission_bps);
+        let data_1 =
+            tda_to_account_shared_data(&tip_distribution_program_id, tip_distro_1_tips, tda_1);
+        let tda_2_fields = (tip_distribution_account_2.0, tda_2.validator_commission_bps);
+        let data_2 =
+            tda_to_account_shared_data(&tip_distribution_program_id, tip_distro_2_tips, tda_2);
+
+        let accounts_data = create_config_account_data(&tip_payment_program_id, &bank);
+        for (pubkey, data) in accounts_data {
+            bank.store_account(&pubkey, &data);
+        }
+
+        bank.store_account(&tip_distribution_account_0.0, &data_0);
+        bank.store_account(&tip_distribution_account_1.0, &data_1);
+        bank.store_account(&tip_distribution_account_2.0, &data_2);
+
+        bank.freeze();
+        let stake_meta_collection = generate_stake_meta_collection(
+            &bank,
+            &tip_distribution_program_id,
+            &tip_payment_program_id,
+        )
+        .unwrap();
+        assert_eq!(
+            stake_meta_collection.tip_distribution_program_id,
+            tip_distribution_program_id
+        );
+        assert_eq!(stake_meta_collection.slot, bank.slot());
+        assert_eq!(stake_meta_collection.epoch, bank.epoch());
+
+        let mut expected_stake_metas = HashMap::new();
+        expected_stake_metas.insert(
+            validator_keypairs_0.vote_keypair.pubkey(),
+            StakeMeta {
+                validator_vote_account: validator_keypairs_0.vote_keypair.pubkey(),
+                delegations: validator_0_delegations.clone(),
+                total_delegated: validator_0_delegations
+                    .iter()
+                    .fold(0u64, |sum, delegation| {
+                        sum.checked_add(delegation.lamports_delegated).unwrap()
+                    }),
+                maybe_tip_distribution_meta: Some(TipDistributionMeta {
+                    merkle_root_upload_authority,
+                    tip_distribution_pubkey: tda_0_fields.0,
+                    total_tips: tip_distro_0_tips
+                        .checked_sub(
+                            bank.get_minimum_balance_for_rent_exemption(
+                                TipDistributionAccount::SIZE,
+                            ),
+                        )
+                        .unwrap(),
+                    validator_fee_bps: tda_0_fields.1,
+                }),
+                commission: 0,
+                validator_node_pubkey: validator_keypairs_0.node_keypair.pubkey(),
+            },
+        );
+        expected_stake_metas.insert(
+            validator_keypairs_1.vote_keypair.pubkey(),
+            StakeMeta {
+                validator_vote_account: validator_keypairs_1.vote_keypair.pubkey(),
+                delegations: validator_1_delegations.clone(),
+                total_delegated: validator_1_delegations
+                    .iter()
+                    .fold(0u64, |sum, delegation| {
+                        sum.checked_add(delegation.lamports_delegated).unwrap()
+                    }),
+                maybe_tip_distribution_meta: Some(TipDistributionMeta {
+                    merkle_root_upload_authority,
+                    tip_distribution_pubkey: tda_1_fields.0,
+                    total_tips: tip_distro_1_tips
+                        .checked_sub(
+                            bank.get_minimum_balance_for_rent_exemption(
+                                TipDistributionAccount::SIZE,
+                            ),
+                        )
+                        .unwrap(),
+                    validator_fee_bps: tda_1_fields.1,
+                }),
+                commission: 0,
+                validator_node_pubkey: validator_keypairs_1.node_keypair.pubkey(),
+            },
+        );
+        expected_stake_metas.insert(
+            validator_keypairs_2.vote_keypair.pubkey(),
+            StakeMeta {
+                validator_vote_account: validator_keypairs_2.vote_keypair.pubkey(),
+                delegations: validator_2_delegations.clone(),
+                total_delegated: validator_2_delegations
+                    .iter()
+                    .fold(0u64, |sum, delegation| {
+                        sum.checked_add(delegation.lamports_delegated).unwrap()
+                    }),
+                maybe_tip_distribution_meta: Some(TipDistributionMeta {
+                    merkle_root_upload_authority,
+                    tip_distribution_pubkey: tda_2_fields.0,
+                    total_tips: tip_distro_2_tips
+                        .checked_sub(
+                            bank.get_minimum_balance_for_rent_exemption(
+                                TipDistributionAccount::SIZE,
+                            ),
+                        )
+                        .unwrap(),
+                    validator_fee_bps: tda_2_fields.1,
+                }),
+                commission: 0,
+                validator_node_pubkey: validator_keypairs_2.node_keypair.pubkey(),
+            },
+        );
+
+        println!(
+            "validator_0 [vote_account={}, stake_account={}]",
+            validator_keypairs_0.vote_keypair.pubkey(),
+            validator_keypairs_0.stake_keypair.pubkey()
+        );
+        println!(
+            "validator_1 [vote_account={}, stake_account={}]",
+            validator_keypairs_1.vote_keypair.pubkey(),
+            validator_keypairs_1.stake_keypair.pubkey()
+        );
+        println!(
+            "validator_2 [vote_account={}, stake_account={}]",
+            validator_keypairs_2.vote_keypair.pubkey(),
+            validator_keypairs_2.stake_keypair.pubkey(),
+        );
+
+        assert_eq!(
+            expected_stake_metas.len(),
+            stake_meta_collection.stake_metas.len()
+        );
+
+        for actual_stake_meta in stake_meta_collection.stake_metas {
+            let expected_stake_meta = expected_stake_metas
+                .get(&actual_stake_meta.validator_vote_account)
+                .unwrap();
+            assert_eq!(
+                expected_stake_meta.maybe_tip_distribution_meta,
+                actual_stake_meta.maybe_tip_distribution_meta
+            );
+            assert_eq!(
+                expected_stake_meta.total_delegated,
+                actual_stake_meta.total_delegated
+            );
+            assert_eq!(expected_stake_meta.commission, actual_stake_meta.commission);
+            assert_eq!(
+                expected_stake_meta.validator_vote_account,
+                actual_stake_meta.validator_vote_account
+            );
+
+            assert_eq!(
+                expected_stake_meta.delegations.len(),
+                actual_stake_meta.delegations.len()
+            );
+
+            for expected_delegation in &expected_stake_meta.delegations {
+                let actual_delegation = actual_stake_meta
+                    .delegations
+                    .iter()
+                    .find(|d| d.stake_account_pubkey == expected_delegation.stake_account_pubkey)
+                    .unwrap();
+
+                assert_eq!(expected_delegation, actual_delegation);
+            }
+        }
+    }
+
+    /// Helper function that sends a delegate stake instruction to the bank.
+    /// Returns the created stake account pubkey.
+    fn delegate_stake_helper(
+        bank: &Bank,
+        from_keypair: &Keypair,
+        vote_account: &Pubkey,
+        delegation_amount: u64,
+    ) -> Pubkey {
+        let minimum_delegation = solana_stake_program::get_minimum_delegation(&bank.feature_set);
+        assert!(
+            delegation_amount >= minimum_delegation,
+            "{}",
+            format!(
+                "received delegation_amount {}, must be at least {}",
+                delegation_amount, minimum_delegation
+            )
+        );
+        if let Some(from_account) = bank.get_account(&from_keypair.pubkey()) {
+            assert_eq!(from_account.owner(), &solana_sdk::system_program::id());
+        } else {
+            panic!("from_account DNE");
+        }
+        assert!(bank.get_account(vote_account).is_some());
+
+        let stake_keypair = Keypair::new();
+        let instructions = stake::instruction::create_account_and_delegate_stake(
+            &from_keypair.pubkey(),
+            &stake_keypair.pubkey(),
+            vote_account,
+            &Authorized::auto(&from_keypair.pubkey()),
+            &Lockup::default(),
+            delegation_amount,
+        );
+
+        let message = Message::new(&instructions[..], Some(&from_keypair.pubkey()));
+        let transaction = Transaction::new(
+            &[from_keypair, &stake_keypair],
+            message,
+            bank.last_blockhash(),
+        );
+
+        bank.process_transaction(&transaction)
+            .map_err(|e| {
+                eprintln!("Error delegating stake [error={}]", e);
+                e
+            })
+            .unwrap();
+
+        stake_keypair.pubkey()
+    }
+
+    fn tda_to_account_shared_data(
+        tip_distribution_program_id: &Pubkey,
+        lamports: u64,
+        tda: TipDistributionAccount,
+    ) -> AccountSharedData {
+        let mut account_data = AccountSharedData::new(
+            lamports,
+            TipDistributionAccount::SIZE,
+            tip_distribution_program_id,
+        );
+
+        let mut data: [u8; TipDistributionAccount::SIZE] = [0u8; TipDistributionAccount::SIZE];
+        let mut cursor = std::io::Cursor::new(&mut data[..]);
+        tda.try_serialize(&mut cursor).unwrap();
+
+        account_data.set_data(data.to_vec());
+        account_data
+    }
+
+    fn create_config_account_data(
+        tip_payment_program_id: &Pubkey,
+        bank: &Bank,
+    ) -> Vec<(Pubkey, AccountSharedData)> {
+        let mut account_datas = vec![];
+
+        let config_pda =
+            Pubkey::find_program_address(&[CONFIG_ACCOUNT_SEED], tip_payment_program_id);
+
+        let tip_accounts = [
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_0], tip_payment_program_id),
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_1], tip_payment_program_id),
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_2], tip_payment_program_id),
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_3], tip_payment_program_id),
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_4], tip_payment_program_id),
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_5], tip_payment_program_id),
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_6], tip_payment_program_id),
+            Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_7], tip_payment_program_id),
+        ];
+
+        let config = Config {
+            tip_receiver: Pubkey::new_unique(),
+            block_builder: Pubkey::new_unique(),
+            block_builder_commission_pct: 10,
+            bumps: InitBumps {
+                config: config_pda.1,
+                tip_payment_account_0: tip_accounts[0].1,
+                tip_payment_account_1: tip_accounts[1].1,
+                tip_payment_account_2: tip_accounts[2].1,
+                tip_payment_account_3: tip_accounts[3].1,
+                tip_payment_account_4: tip_accounts[4].1,
+                tip_payment_account_5: tip_accounts[5].1,
+                tip_payment_account_6: tip_accounts[6].1,
+                tip_payment_account_7: tip_accounts[7].1,
+            },
+        };
+
+        let mut config_account_data = AccountSharedData::new(
+            bank.get_minimum_balance_for_rent_exemption(Config::SIZE),
+            Config::SIZE,
+            tip_payment_program_id,
+        );
+
+        let mut config_data: [u8; Config::SIZE] = [0u8; Config::SIZE];
+        let mut config_cursor = std::io::Cursor::new(&mut config_data[..]);
+        config.try_serialize(&mut config_cursor).unwrap();
+        config_account_data.set_data(config_data.to_vec());
+        account_datas.push((config_pda.0, config_account_data));
+
+        account_datas.extend(tip_accounts.into_iter().map(|(pubkey, _)| {
+            let mut tip_account_data = AccountSharedData::new(
+                bank.get_minimum_balance_for_rent_exemption(TipPaymentAccount::SIZE),
+                TipPaymentAccount::SIZE,
+                tip_payment_program_id,
+            );
+
+            let mut data: [u8; TipPaymentAccount::SIZE] = [0u8; TipPaymentAccount::SIZE];
+            let mut cursor = std::io::Cursor::new(&mut data[..]);
+            TipPaymentAccount::default()
+                .try_serialize(&mut cursor)
+                .unwrap();
+            tip_account_data.set_data(data.to_vec());
+
+            (pubkey, tip_account_data)
+        }));
+
+        account_datas
+    }
+}
diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs
index 7c9877a26b..7a585d3c9d 100644
--- a/transaction-status/src/lib.rs
+++ b/transaction-status/src/lib.rs
@@ -25,7 +25,7 @@ use {
         },
         transaction_context::TransactionReturnData,
     },
-    std::fmt,
+    std::{collections::HashMap, fmt},
     thiserror::Error,
 };
 
@@ -278,6 +278,13 @@ impl From<InnerInstructions> for UiInnerInstructions {
     }
 }
 
+#[derive(Default)]
+pub struct PreBalanceInfo {
+    pub native: Vec<Vec<u64>>,
+    pub token: Vec<Vec<TransactionTokenBalance>>,
+    pub mint_decimals: HashMap<Pubkey, u8>,
+}
+
 #[derive(Clone, Debug, PartialEq)]
 pub struct TransactionTokenBalance {
     pub account_index: u8,
diff --git a/validator/Cargo.toml b/validator/Cargo.toml
index 03d14f17d4..865724114c 100644
--- a/validator/Cargo.toml
+++ b/validator/Cargo.toml
@@ -53,6 +53,7 @@ solana-rpc = { workspace = true }
 solana-rpc-client = { workspace = true }
 solana-rpc-client-api = { workspace = true }
 solana-runtime = { workspace = true }
+solana-runtime-plugin = { workspace = true }
 solana-sdk = { workspace = true }
 solana-send-transaction-service = { workspace = true }
 solana-storage-bigtable = { workspace = true }
@@ -63,6 +64,7 @@ solana-version = { workspace = true }
 solana-vote-program = { workspace = true }
 symlink = { workspace = true }
 thiserror = { workspace = true }
+tonic = { workspace = true, features = ["tls", "tls-roots", "tls-webpki-roots"] }
 
 [dev-dependencies]
 solana-account-decoder = { workspace = true }
diff --git a/validator/src/admin_rpc_service.rs b/validator/src/admin_rpc_service.rs
index b377c219ed..2be51d513e 100644
--- a/validator/src/admin_rpc_service.rs
+++ b/validator/src/admin_rpc_service.rs
@@ -10,8 +10,14 @@ use {
     log::*,
     serde::{de::Deserializer, Deserialize, Serialize},
     solana_core::{
-        admin_rpc_post_init::AdminRpcRequestMetadataPostInit, consensus::Tower,
-        tower_storage::TowerStorage, validator::ValidatorStartProgress,
+        admin_rpc_post_init::AdminRpcRequestMetadataPostInit,
+        consensus::Tower,
+        proxy::{
+            block_engine_stage::{BlockEngineConfig, BlockEngineStage},
+            relayer_stage::{RelayerConfig, RelayerStage},
+        },
+        tower_storage::TowerStorage,
+        validator::ValidatorStartProgress,
     },
     solana_geyser_plugin_manager::GeyserPluginManagerRequest,
     solana_gossip::contact_info::{ContactInfo, Protocol, SOCKET_ADDR_UNSPECIFIED},
@@ -29,6 +35,7 @@ use {
         fmt::{self, Display},
         net::SocketAddr,
         path::{Path, PathBuf},
+        str::FromStr,
         sync::{Arc, RwLock},
         thread::{self, Builder},
         time::{Duration, SystemTime},
@@ -233,6 +240,27 @@ pub trait AdminRpc {
         meta: Self::Metadata,
         public_tpu_forwards_addr: SocketAddr,
     ) -> Result<()>;
+
+    #[rpc(meta, name = "setBlockEngineConfig")]
+    fn set_block_engine_config(
+        &self,
+        meta: Self::Metadata,
+        block_engine_url: String,
+        trust_packets: bool,
+    ) -> Result<()>;
+
+    #[rpc(meta, name = "setRelayerConfig")]
+    fn set_relayer_config(
+        &self,
+        meta: Self::Metadata,
+        relayer_url: String,
+        trust_packets: bool,
+        expected_heartbeat_interval_ms: u64,
+        max_failed_heartbeats: u64,
+    ) -> Result<()>;
+
+    #[rpc(meta, name = "setShredReceiverAddress")]
+    fn set_shred_receiver_address(&self, meta: Self::Metadata, addr: String) -> Result<()>;
 }
 
 pub struct AdminRpcImpl;
@@ -431,6 +459,30 @@ impl AdminRpc for AdminRpcImpl {
         Ok(())
     }
 
+    fn set_block_engine_config(
+        &self,
+        meta: Self::Metadata,
+        block_engine_url: String,
+        trust_packets: bool,
+    ) -> Result<()> {
+        debug!("set_block_engine_config request received");
+        let config = BlockEngineConfig {
+            block_engine_url,
+            trust_packets,
+        };
+        // Detailed log messages are printed inside validate function
+        if BlockEngineStage::is_valid_block_engine_config(&config) {
+            meta.with_post_init(|post_init| {
+                *post_init.block_engine_config.lock().unwrap() = config;
+                Ok(())
+            })
+        } else {
+            Err(jsonrpc_core::error::Error::invalid_params(
+                "failed to set block engine config. see logs for details.",
+            ))
+        }
+    }
+
     fn set_identity(
         &self,
         meta: Self::Metadata,
@@ -465,6 +517,55 @@ impl AdminRpc for AdminRpcImpl {
         AdminRpcImpl::set_identity_keypair(meta, identity_keypair, require_tower)
     }
 
+    fn set_relayer_config(
+        &self,
+        meta: Self::Metadata,
+        relayer_url: String,
+        trust_packets: bool,
+        expected_heartbeat_interval_ms: u64,
+        max_failed_heartbeats: u64,
+    ) -> Result<()> {
+        debug!("set_relayer_config request received");
+        let expected_heartbeat_interval = Duration::from_millis(expected_heartbeat_interval_ms);
+        let oldest_allowed_heartbeat =
+            Duration::from_millis(max_failed_heartbeats * expected_heartbeat_interval_ms);
+        let config = RelayerConfig {
+            relayer_url,
+            expected_heartbeat_interval,
+            oldest_allowed_heartbeat,
+            trust_packets,
+        };
+        // Detailed log messages are printed inside validate function
+        if RelayerStage::is_valid_relayer_config(&config) {
+            meta.with_post_init(|post_init| {
+                *post_init.relayer_config.lock().unwrap() = config;
+                Ok(())
+            })
+        } else {
+            Err(jsonrpc_core::error::Error::invalid_params(
+                "failed to set relayer config. see logs for details.",
+            ))
+        }
+    }
+
+    fn set_shred_receiver_address(&self, meta: Self::Metadata, addr: String) -> Result<()> {
+        let shred_receiver_address = if addr.is_empty() {
+            None
+        } else {
+            Some(SocketAddr::from_str(&addr).map_err(|_| {
+                jsonrpc_core::error::Error::invalid_params(format!(
+                    "invalid shred receiver address: {}",
+                    addr
+                ))
+            })?)
+        };
+
+        meta.with_post_init(|post_init| {
+            *post_init.shred_receiver_address.write().unwrap() = shred_receiver_address;
+            Ok(())
+        })
+    }
+
     fn set_staked_nodes_overrides(&self, meta: Self::Metadata, path: String) -> Result<()> {
         let loaded_config = load_staked_nodes_overrides(&path)
             .map_err(|err| {
@@ -839,7 +940,10 @@ mod tests {
             solana_program::{program_option::COption, program_pack::Pack},
             state::{Account as TokenAccount, AccountState as TokenAccountState, Mint},
         },
-        std::{collections::HashSet, sync::atomic::AtomicBool},
+        std::{
+            collections::HashSet,
+            sync::{atomic::AtomicBool, Mutex},
+        },
     };
 
     #[derive(Default)]
@@ -877,6 +981,9 @@ mod tests {
             let vote_account = vote_keypair.pubkey();
             let start_progress = Arc::new(RwLock::new(ValidatorStartProgress::default()));
             let repair_whitelist = Arc::new(RwLock::new(HashSet::new()));
+            let block_engine_config = Arc::new(Mutex::new(BlockEngineConfig::default()));
+            let relayer_config = Arc::new(Mutex::new(RelayerConfig::default()));
+            let shred_receiver_address = Arc::new(RwLock::new(None));
             let meta = AdminRpcRequestMetadata {
                 rpc_addr: None,
                 start_time: SystemTime::now(),
@@ -889,6 +996,9 @@ mod tests {
                     bank_forks: bank_forks.clone(),
                     vote_account,
                     repair_whitelist,
+                    block_engine_config,
+                    relayer_config,
+                    shred_receiver_address,
                 }))),
                 staked_nodes_overrides: Arc::new(RwLock::new(HashMap::new())),
                 rpc_to_plugin_manager_sender: None,
diff --git a/validator/src/bootstrap.rs b/validator/src/bootstrap.rs
index c2777189db..20705c1d03 100644
--- a/validator/src/bootstrap.rs
+++ b/validator/src/bootstrap.rs
@@ -817,12 +817,13 @@ fn get_highest_local_snapshot_hash(
     incremental_snapshot_archives_dir: impl AsRef<Path>,
     incremental_snapshot_fetch: bool,
 ) -> Option<(Slot, Hash)> {
-    snapshot_utils::get_highest_full_snapshot_archive_info(full_snapshot_archives_dir)
+    snapshot_utils::get_highest_full_snapshot_archive_info(full_snapshot_archives_dir, None)
         .and_then(|full_snapshot_info| {
             if incremental_snapshot_fetch {
                 snapshot_utils::get_highest_incremental_snapshot_archive_info(
                     incremental_snapshot_archives_dir,
                     full_snapshot_info.slot(),
+                    None,
                 )
                 .map(|incremental_snapshot_info| {
                     (
@@ -970,7 +971,11 @@ fn build_known_snapshot_hashes<'a>(
     }
 
     'to_next_node: for node in nodes {
-        let Some(SnapshotHash {full: full_snapshot_hash, incr: incremental_snapshot_hash}) = get_snapshot_hashes_for_node(node) else {
+        let Some(SnapshotHash {
+            full: full_snapshot_hash,
+            incr: incremental_snapshot_hash,
+        }) = get_snapshot_hashes_for_node(node)
+        else {
             continue 'to_next_node;
         };
 
diff --git a/validator/src/cli.rs b/validator/src/cli.rs
index 0b3f53009c..154088e193 100644
--- a/validator/src/cli.rs
+++ b/validator/src/cli.rs
@@ -56,6 +56,10 @@ const MAX_SNAPSHOT_DOWNLOAD_ABORT: u32 = 5;
 // with less than 2 ticks per slot.
 const MINIMUM_TICKS_PER_SLOT: u64 = 2;
 
+const DEFAULT_PREALLOCATED_BUNDLE_COST: &str = "3000000";
+const DEFAULT_RELAYER_EXPECTED_HEARTBEAT_INTERVAL_MS: &str = "500";
+const DEFAULT_RELAYER_MAX_FAILED_HEARTBEATS: &str = "3";
+
 pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
     return App::new(crate_name!()).about(crate_description!())
         .version(version)
@@ -66,6 +70,87 @@ pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
                 .long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
                 .help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
         )
+        .arg(
+            Arg::with_name("block_engine_url")
+                .long("block-engine-url")
+                .help("Block engine url.  Set to empty string to disable block engine connection.")
+                .takes_value(true)
+        )
+        .arg(
+            Arg::with_name("relayer_url")
+                .long("relayer-url")
+                .help("Relayer url. Set to empty string to disable relayer connection.")
+                .takes_value(true)
+        )
+        .arg(
+            Arg::with_name("trust_relayer_packets")
+                .long("trust-relayer-packets")
+                .takes_value(false)
+                .help("Skip signature verification on relayer packets. Not recommended unless the relayer is trusted.")
+        )
+        .arg(
+            Arg::with_name("relayer_expected_heartbeat_interval_ms")
+                .long("relayer-expected-heartbeat-interval-ms")
+                .takes_value(true)
+                .help("Interval at which the Relayer is expected to send heartbeat messages.")
+                .default_value(DEFAULT_RELAYER_EXPECTED_HEARTBEAT_INTERVAL_MS)
+        )
+        .arg(
+            Arg::with_name("relayer_max_failed_heartbeats")
+                .long("relayer-max-failed-heartbeats")
+                .takes_value(true)
+                .help("Maximum number of heartbeats the Relayer can miss before falling back to the normal TPU pipeline.")
+                .default_value(DEFAULT_RELAYER_MAX_FAILED_HEARTBEATS)
+        )
+        .arg(
+            Arg::with_name("trust_block_engine_packets")
+                .long("trust-block-engine-packets")
+                .takes_value(false)
+                .help("Skip signature verification on block engine packets. Not recommended unless the block engine is trusted.")
+        )
+        .arg(
+            Arg::with_name("tip_payment_program_pubkey")
+                .long("tip-payment-program-pubkey")
+                .value_name("TIP_PAYMENT_PROGRAM_PUBKEY")
+                .takes_value(true)
+                .help("The public key of the tip-payment program")
+        )
+        .arg(
+            Arg::with_name("tip_distribution_program_pubkey")
+                .long("tip-distribution-program-pubkey")
+                .value_name("TIP_DISTRIBUTION_PROGRAM_PUBKEY")
+                .takes_value(true)
+                .help("The public key of the tip-distribution program.")
+        )
+        .arg(
+            Arg::with_name("merkle_root_upload_authority")
+                .long("merkle-root-upload-authority")
+                .value_name("MERKLE_ROOT_UPLOAD_AUTHORITY")
+                .takes_value(true)
+                .help("The public key of the authorized merkle-root uploader.")
+        )
+        .arg(
+            Arg::with_name("commission_bps")
+                .long("commission-bps")
+                .value_name("COMMISSION_BPS")
+                .takes_value(true)
+                .help("The commission validator takes from tips expressed in basis points.")
+        )
+        .arg(
+            Arg::with_name("preallocated_bundle_cost")
+                .long("preallocated-bundle-cost")
+                .value_name("PREALLOCATED_BUNDLE_COST")
+                .takes_value(true)
+                .default_value(DEFAULT_PREALLOCATED_BUNDLE_COST)
+                .help("Number of CUs to allocate for bundles at beginning of slot.")
+        )
+        .arg(
+            Arg::with_name("shred_receiver_address")
+                .long("shred-receiver-address")
+                .value_name("SHRED_RECEIVER_ADDRESS")
+                .takes_value(true)
+                .help("Validator will forward all shreds to this address in addition to normal turbine operation. Set to empty string to disable.")
+        )
         .arg(
             Arg::with_name("identity")
                 .short("i")
@@ -1097,6 +1182,14 @@ pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
                 .multiple(true)
                 .help("Specify the configuration file for the Geyser plugin."),
         )
+        .arg(
+            Arg::with_name("runtime_plugin_config")
+                .long("runtime-plugin-config")
+                .value_name("FILE")
+                .takes_value(true)
+                .multiple(true)
+                .help("Specify the configuration file for a Runtime plugin."),
+        )
         .arg(
             Arg::with_name("snapshot_archive_format")
                 .long("snapshot-archive-format")
@@ -1382,6 +1475,68 @@ pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
         )
         .args(&get_deprecated_arguments())
         .after_help("The default subcommand is run")
+        .subcommand(
+            SubCommand::with_name("set-block-engine-config")
+                .about("Set configuration for connection to a block engine")
+                .arg(
+                    Arg::with_name("block_engine_url")
+                        .long("block-engine-url")
+                        .help("Block engine url.  Set to empty string to disable block engine connection.")
+                        .takes_value(true)
+                        .required(true)
+                )
+                .arg(
+                    Arg::with_name("trust_block_engine_packets")
+                        .long("trust-block-engine-packets")
+                        .takes_value(false)
+                        .help("Skip signature verification on block engine packets. Not recommended unless the block engine is trusted.")
+                )
+        )
+        .subcommand(
+            SubCommand::with_name("set-relayer-config")
+                .about("Set configuration for connection to a relayer")
+                .arg(
+                    Arg::with_name("relayer_url")
+                        .long("relayer-url")
+                        .help("Relayer url. Set to empty string to disable relayer connection.")
+                        .takes_value(true)
+                        .required(true)
+                )
+                .arg(
+                    Arg::with_name("trust_relayer_packets")
+                        .long("trust-relayer-packets")
+                        .takes_value(false)
+                        .help("Skip signature verification on relayer packets. Not recommended unless the relayer is trusted.")
+                )
+                .arg(
+                    Arg::with_name("relayer_expected_heartbeat_interval_ms")
+                        .long("relayer-expected-heartbeat-interval-ms")
+                        .takes_value(true)
+                        .help("Interval at which the Relayer is expected to send heartbeat messages.")
+                        .required(false)
+                        .default_value(DEFAULT_RELAYER_EXPECTED_HEARTBEAT_INTERVAL_MS)
+                )
+                .arg(
+                    Arg::with_name("relayer_max_failed_heartbeats")
+                        .long("relayer-max-failed-heartbeats")
+                        .takes_value(true)
+                        .help("Maximum number of heartbeats the Relayer can miss before falling back to the normal TPU pipeline.")
+                        .required(false)
+                        .default_value(DEFAULT_RELAYER_MAX_FAILED_HEARTBEATS)
+                )
+        )
+        .subcommand(
+            SubCommand::with_name("set-shred-receiver-address")
+                .about("Changes shred receiver address")
+                .arg(
+                    Arg::with_name("shred_receiver_address")
+                        .long("shred-receiver-address")
+                        .value_name("SHRED_RECEIVER_ADDRESS")
+                        .takes_value(true)
+                        .help("Validator will forward all shreds to this address in addition to normal turbine operation. Set to empty string to disable.")
+                        .required(true)
+                )
+        )
         .subcommand(
             SubCommand::with_name("exit")
                 .about("Send an exit request to the validator")
@@ -1515,6 +1670,48 @@ pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> {
             SubCommand::with_name("run")
                 .about("Run the validator")
         )
+        .subcommand(
+            SubCommand::with_name("runtime-plugin")
+                .about("Manage and view runtime plugins")
+                .setting(AppSettings::SubcommandRequiredElseHelp)
+                .setting(AppSettings::InferSubcommands)
+                .subcommand(
+                    SubCommand::with_name("list")
+                        .about("List all current running runtime plugins")
+                )
+                .subcommand(
+                    SubCommand::with_name("unload")
+                        .about("Unload a particular runtime plugin. You must specify the runtime plugin name")
+                        .arg(
+                            Arg::with_name("name")
+                                .required(true)
+                                .takes_value(true)
+                        )
+                )
+                .subcommand(
+                    SubCommand::with_name("reload")
+                        .about("Reload a particular runtime plugin. You must specify the runtime plugin name and the new config path")
+                        .arg(
+                            Arg::with_name("name")
+                                .required(true)
+                                .takes_value(true)
+                        )
+                        .arg(
+                            Arg::with_name("config")
+                                .required(true)
+                                .takes_value(true)
+                        )
+                )
+                .subcommand(
+                    SubCommand::with_name("load")
+                        .about("Load a new gesyer plugin. You must specify the config path. Fails if overwriting (use reload)")
+                        .arg(
+                            Arg::with_name("config")
+                                .required(true)
+                                .takes_value(true)
+                        )
+                )
+        )
         .subcommand(
             SubCommand::with_name("plugin")
                 .about("Manage and view geyser plugins")
@@ -1729,6 +1926,22 @@ fn deprecated_arguments() -> Vec<DeprecatedArg> {
             .help("Enables faster starting of validators by skipping startup clean and shrink."),
         usage_warning: "Enabled by default",
     );
+    add_arg!(
+        Arg::with_name("block_engine_address")
+            .long("block-engine-address")
+            .value_name("block_engine_address")
+            .takes_value(true)
+            .help("Deprecated: Please use block_engine_url.")
+            .conflicts_with("block_engine_url"),
+        replaced_by: "block-engine-url");
+    add_arg!(
+        Arg::with_name("block_engine_auth_service_address")
+                .long("block-engine-auth-service-address")
+                .value_name("block_engine_auth_service_address")
+                .takes_value(true)
+                .help("Deprecated: Please use block_engine_url.")
+                .conflicts_with("block_engine_url"),
+        replaced_by: "block-engine-url");
     add_arg!(
         Arg::with_name("disable_quic_servers")
             .long("disable-quic-servers")
@@ -1796,6 +2009,22 @@ fn deprecated_arguments() -> Vec<DeprecatedArg> {
         .long("no-rocksdb-compaction")
         .takes_value(false)
         .help("Disable manual compaction of the ledger database"));
+    add_arg!(
+        Arg::with_name("relayer_address")
+                .long("relayer-address")
+                .value_name("relayer_address")
+                .takes_value(true)
+                .help("Deprecated: Please use relayer_url.")
+                .conflicts_with("relayer_url"),
+        replaced_by: "relayer-url");
+    add_arg!(
+        Arg::with_name("relayer_auth_service_address")
+                .long("relayer-auth-service-address")
+                .value_name("relayer_auth_service_address")
+                .takes_value(true)
+                .help("Deprecated: Please use relayer_url.")
+                .conflicts_with("relayer_url"),
+        replaced_by: "relayer-url");
     add_arg!(Arg::with_name("rocksdb_compaction_interval")
         .long("rocksdb-compaction-interval-slots")
         .value_name("ROCKSDB_COMPACTION_INTERVAL_SLOTS")
@@ -2444,6 +2673,14 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App<
                 .multiple(true)
                 .help("Specify the configuration file for the Geyser plugin."),
         )
+        .arg(
+            Arg::with_name("runtime_plugin_config")
+                .long("runtime-plugin-config")
+                .value_name("FILE")
+                .takes_value(true)
+                .multiple(true)
+                .help("Specify the configuration file for a Runtime plugin."),
+        )
         .arg(
             Arg::with_name("deactivate_feature")
                 .long("deactivate-feature")
diff --git a/validator/src/dashboard.rs b/validator/src/dashboard.rs
index a904172cda..d458e21530 100644
--- a/validator/src/dashboard.rs
+++ b/validator/src/dashboard.rs
@@ -274,6 +274,7 @@ fn get_validator_stats(
         Ok(()) => "ok".to_string(),
         Err(err) => {
             if let client_error::ErrorKind::RpcError(request::RpcError::RpcResponseError {
+                request_id: _,
                 code: _,
                 message: _,
                 data:
diff --git a/validator/src/main.rs b/validator/src/main.rs
index 4e699b382f..eb345bee4e 100644
--- a/validator/src/main.rs
+++ b/validator/src/main.rs
@@ -1,17 +1,21 @@
 #![allow(clippy::integer_arithmetic)]
+
 #[cfg(not(target_env = "msvc"))]
 use jemallocator::Jemalloc;
 use {
     clap::{crate_name, value_t, value_t_or_exit, values_t, values_t_or_exit, ArgMatches},
     console::style,
     crossbeam_channel::unbounded,
+    jsonrpc_server_utils::tokio::runtime::Runtime,
     log::*,
     rand::{seq::SliceRandom, thread_rng},
     solana_clap_utils::input_parsers::{keypair_of, keypairs_of, pubkey_of, value_of},
     solana_core::{
         banking_trace::DISABLED_BAKING_TRACE_DIR,
         ledger_cleanup_service::{DEFAULT_MAX_LEDGER_SHREDS, DEFAULT_MIN_MAX_LEDGER_SHREDS},
+        proxy::{block_engine_stage::BlockEngineConfig, relayer_stage::RelayerConfig},
         system_monitor_service::SystemMonitorService,
+        tip_manager::{TipDistributionAccountConfig, TipManagerConfig},
         tower_storage,
         tpu::DEFAULT_TPU_COALESCE,
         validator::{
@@ -48,6 +52,10 @@ use {
             ArchiveFormat, SnapshotVersion,
         },
     },
+    solana_runtime_plugin::{
+        runtime_plugin_admin_rpc_service,
+        runtime_plugin_admin_rpc_service::RuntimePluginAdminRpcRequestMetadata,
+    },
     solana_sdk::{
         clock::{Slot, DEFAULT_S_PER_SLOT},
         commitment_config::CommitmentConfig,
@@ -76,7 +84,7 @@ use {
         path::{Path, PathBuf},
         process::exit,
         str::FromStr,
-        sync::{Arc, RwLock},
+        sync::{atomic::AtomicBool, Arc, Mutex, RwLock},
         time::{Duration, SystemTime},
     },
 };
@@ -465,6 +473,60 @@ pub fn main() {
 
     let operation = match matches.subcommand() {
         ("", _) | ("run", _) => Operation::Run,
+        ("set-block-engine-config", Some(subcommand_matches)) => {
+            let block_engine_url = value_t_or_exit!(subcommand_matches, "block_engine_url", String);
+            let trust_packets = subcommand_matches.is_present("trust_block_engine_packets");
+            let admin_client = admin_rpc_service::connect(&ledger_path);
+            admin_rpc_service::runtime()
+                .block_on(async move {
+                    admin_client
+                        .await?
+                        .set_block_engine_config(block_engine_url, trust_packets)
+                        .await
+                })
+                .unwrap_or_else(|err| {
+                    println!("set block engine config failed: {}", err);
+                    exit(1);
+                });
+            return;
+        }
+        ("set-relayer-config", Some(subcommand_matches)) => {
+            let relayer_url = value_t_or_exit!(subcommand_matches, "relayer_url", String);
+            let trust_packets = subcommand_matches.is_present("trust_relayer_packets");
+            let expected_heartbeat_interval_ms: u64 =
+                value_of(subcommand_matches, "relayer_expected_heartbeat_interval_ms").unwrap();
+            let max_failed_heartbeats: u64 =
+                value_of(subcommand_matches, "relayer_max_failed_heartbeats").unwrap();
+            let admin_client = admin_rpc_service::connect(&ledger_path);
+            admin_rpc_service::runtime()
+                .block_on(async move {
+                    admin_client
+                        .await?
+                        .set_relayer_config(
+                            relayer_url,
+                            trust_packets,
+                            expected_heartbeat_interval_ms,
+                            max_failed_heartbeats,
+                        )
+                        .await
+                })
+                .unwrap_or_else(|err| {
+                    println!("set relayer config failed: {}", err);
+                    exit(1);
+                });
+            return;
+        }
+        ("set-shred-receiver-address", Some(subcommand_matches)) => {
+            let addr = value_t_or_exit!(subcommand_matches, "shred_receiver_address", String);
+            let admin_client = admin_rpc_service::connect(&ledger_path);
+            admin_rpc_service::runtime()
+                .block_on(async move { admin_client.await?.set_shred_receiver_address(addr).await })
+                .unwrap_or_else(|err| {
+                    println!("set shred receiver address failed: {}", err);
+                    exit(1);
+                });
+            return;
+        }
         ("authorized-voter", Some(authorized_voter_subcommand_matches)) => {
             match authorized_voter_subcommand_matches.subcommand() {
                 ("add", Some(subcommand_matches)) => {
@@ -616,6 +678,92 @@ pub fn main() {
                 _ => unreachable!(),
             }
         }
+        ("runtime-plugin", Some(plugin_subcommand_matches)) => {
+            let runtime_plugin_rpc_client = runtime_plugin_admin_rpc_service::connect(&ledger_path);
+            let runtime = Runtime::new().unwrap();
+            match plugin_subcommand_matches.subcommand() {
+                ("list", _) => {
+                    let plugins = runtime
+                        .block_on(
+                            async move { runtime_plugin_rpc_client.await?.list_plugins().await },
+                        )
+                        .unwrap_or_else(|err| {
+                            println!("Failed to list plugins: {err}");
+                            exit(1);
+                        });
+                    if !plugins.is_empty() {
+                        println!("Currently the following plugins are loaded:");
+                        for (plugin, i) in plugins.into_iter().zip(1..) {
+                            println!("  {i}) {plugin}");
+                        }
+                    } else {
+                        println!("There are currently no plugins loaded");
+                    }
+                    return;
+                }
+                ("unload", Some(subcommand_matches)) => {
+                    if let Ok(name) = value_t!(subcommand_matches, "name", String) {
+                        runtime
+                            .block_on(async {
+                                runtime_plugin_rpc_client
+                                    .await?
+                                    .unload_plugin(name.clone())
+                                    .await
+                            })
+                            .unwrap_or_else(|err| {
+                                println!("Failed to unload plugin {name}: {err:?}");
+                                exit(1);
+                            });
+                        println!("Successfully unloaded plugin: {name}");
+                    }
+                    return;
+                }
+                ("load", Some(subcommand_matches)) => {
+                    if let Ok(config) = value_t!(subcommand_matches, "config", String) {
+                        let name = runtime
+                            .block_on(async {
+                                runtime_plugin_rpc_client
+                                    .await?
+                                    .load_plugin(config.clone())
+                                    .await
+                            })
+                            .unwrap_or_else(|err| {
+                                println!("Failed to load plugin {config}: {err:?}");
+                                exit(1);
+                            });
+                        println!("Successfully loaded plugin: {name}");
+                    }
+                    return;
+                }
+                ("reload", Some(subcommand_matches)) => {
+                    if let Ok(name) = value_t!(subcommand_matches, "name", String) {
+                        if let Ok(config) = value_t!(subcommand_matches, "config", String) {
+                            println!(
+                                "This command does not work as intended on some systems.\
+                                To correctly reload an existing plugin make sure to:\
+                                    1. Rename the new plugin binary file.\
+                                    2. Unload the previous version.\
+                                    3. Load the new, renamed binary using the 'Load' command."
+                            );
+                            runtime
+                                .block_on(async {
+                                    runtime_plugin_rpc_client
+                                        .await?
+                                        .reload_plugin(name.clone(), config.clone())
+                                        .await
+                                })
+                                .unwrap_or_else(|err| {
+                                    println!("Failed to reload plugin {name}: {err:?}");
+                                    exit(1);
+                                });
+                            println!("Successfully reloaded plugin: {name}");
+                        }
+                    }
+                    return;
+                }
+                _ => unreachable!(),
+            }
+        }
         ("contact-info", Some(subcommand_matches)) => {
             let output_mode = subcommand_matches.value_of("output");
             let admin_client = admin_rpc_service::connect(&ledger_path);
@@ -1254,6 +1402,44 @@ pub fn main() {
     }
     let full_api = matches.is_present("full_rpc_api");
 
+    let voting_disabled = matches.is_present("no_voting") || restricted_repair_only_mode;
+    let tip_manager_config = tip_manager_config_from_matches(&matches, voting_disabled);
+
+    let block_engine_config = BlockEngineConfig {
+        block_engine_url: if matches.is_present("block_engine_url") {
+            value_of(&matches, "block_engine_url").expect("couldn't parse block_engine_url")
+        } else {
+            "".to_string()
+        },
+        trust_packets: matches.is_present("trust_block_engine_packets"),
+    };
+
+    // Defaults are set in cli definition, safe to use unwrap() here
+    let expected_heartbeat_interval_ms: u64 =
+        value_of(&matches, "relayer_expected_heartbeat_interval_ms").unwrap();
+    assert!(
+        expected_heartbeat_interval_ms > 0,
+        "relayer-max-failed-heartbeats must be greater than zero"
+    );
+    let max_failed_heartbeats: u64 = value_of(&matches, "relayer_max_failed_heartbeats").unwrap();
+    assert!(
+        max_failed_heartbeats > 0,
+        "relayer-max-failed-heartbeats must be greater than zero"
+    );
+
+    let relayer_config = RelayerConfig {
+        relayer_url: if matches.is_present("relayer_url") {
+            value_of(&matches, "relayer_url").expect("couldn't parse relayer_url")
+        } else {
+            "".to_string()
+        },
+        expected_heartbeat_interval: Duration::from_millis(expected_heartbeat_interval_ms),
+        oldest_allowed_heartbeat: Duration::from_millis(
+            max_failed_heartbeats * expected_heartbeat_interval_ms,
+        ),
+        trust_packets: matches.is_present("trust_relayer_packets"),
+    };
+
     let mut validator_config = ValidatorConfig {
         require_tower: matches.is_present("require_tower"),
         tower_storage,
@@ -1386,8 +1572,18 @@ pub fn main() {
             log_messages_bytes_limit: value_of(&matches, "log_messages_bytes_limit"),
             ..RuntimeConfig::default()
         },
+        relayer_config: Arc::new(Mutex::new(relayer_config)),
+        block_engine_config: Arc::new(Mutex::new(block_engine_config)),
+        tip_manager_config,
+        shred_receiver_address: Arc::new(RwLock::new(
+            matches
+                .value_of("shred_receiver_address")
+                .map(|addr| SocketAddr::from_str(addr).expect("shred_receiver_address invalid")),
+        )),
         staked_nodes_overrides: staked_nodes_overrides.clone(),
         replay_slots_concurrently: matches.is_present("replay_slots_concurrently"),
+        preallocated_bundle_cost: value_of(&matches, "preallocated_bundle_cost")
+            .expect("preallocated_bundle_cost set as default"),
         ..ValidatorConfig::default()
     };
 
@@ -1698,6 +1894,31 @@ pub fn main() {
         },
     );
 
+    let runtime_plugin_config_and_rpc_rx = {
+        let plugin_exit = Arc::new(AtomicBool::new(false));
+        let (rpc_request_sender, rpc_request_receiver) = unbounded();
+        runtime_plugin_admin_rpc_service::run(
+            &ledger_path,
+            RuntimePluginAdminRpcRequestMetadata {
+                rpc_request_sender,
+                validator_exit: validator_config.validator_exit.clone(),
+            },
+            plugin_exit,
+        );
+
+        if matches.is_present("runtime_plugin_config") {
+            (
+                values_t_or_exit!(matches, "runtime_plugin_config", String)
+                    .into_iter()
+                    .map(PathBuf::from)
+                    .collect(),
+                rpc_request_receiver,
+            )
+        } else {
+            (vec![], rpc_request_receiver)
+        }
+    };
+
     let gossip_host: IpAddr = matches
         .value_of("gossip_host")
         .map(|gossip_host| {
@@ -1871,6 +2092,7 @@ pub fn main() {
         tpu_connection_pool_size,
         tpu_enable_udp,
         admin_service_post_init,
+        Some(runtime_plugin_config_and_rpc_rx),
     )
     .unwrap_or_else(|e| {
         error!("Failed to start validator: {:?}", e);
@@ -1936,3 +2158,47 @@ fn process_account_indexes(matches: &ArgMatches) -> AccountSecondaryIndexes {
         indexes: account_indexes,
     }
 }
+
+fn tip_manager_config_from_matches(
+    matches: &ArgMatches,
+    voting_disabled: bool,
+) -> TipManagerConfig {
+    TipManagerConfig {
+        tip_payment_program_id: pubkey_of(matches, "tip_payment_program_pubkey").unwrap_or_else(
+            || {
+                if !voting_disabled {
+                    panic!("--tip-payment-program-pubkey argument required when validator is voting");
+                }
+                Pubkey::new_unique()
+            },
+        ),
+        tip_distribution_program_id: pubkey_of(matches, "tip_distribution_program_pubkey")
+            .unwrap_or_else(|| {
+                if !voting_disabled {
+                    panic!("--tip-distribution-program-pubkey argument required when validator is voting");
+                }
+                Pubkey::new_unique()
+            }),
+        tip_distribution_account_config: TipDistributionAccountConfig {
+            merkle_root_upload_authority: pubkey_of(matches, "merkle_root_upload_authority")
+                .unwrap_or_else(|| {
+                    if !voting_disabled {
+                        panic!("--merkle-root-upload-authority argument required when validator is voting");
+                    }
+                    Pubkey::new_unique()
+                }),
+            vote_account: pubkey_of(matches, "vote_account").unwrap_or_else(|| {
+                if !voting_disabled {
+                    panic!("--vote-account argument required when validator is voting");
+                }
+                Pubkey::new_unique()
+            }),
+            commission_bps: value_t!(matches, "commission_bps", u16).unwrap_or_else(|_| {
+                if !voting_disabled {
+                    panic!("--commission-bps argument required when validator is voting");
+                }
+                0
+            }),
+        },
+    }
+}
diff --git a/version/src/lib.rs b/version/src/lib.rs
index edeca08c96..68ce039318 100644
--- a/version/src/lib.rs
+++ b/version/src/lib.rs
@@ -63,7 +63,7 @@ impl Default for Version {
             commit: compute_commit(option_env!("CI_COMMIT")).unwrap_or_default(),
             feature_set,
             // Other client implementations need to modify this line.
-            client: u16::try_from(ClientId::SolanaLabs).unwrap(),
+            client: u16::try_from(ClientId::JitoLabs).unwrap(),
         }
     }
 }