Repository: ProvableHQ/snarkOS
Pull request #3471: [Feature] Batch proposal spend limits
Author: login: niklaslong
Labels: name: v3.5.0
Created: 2025-01-22T14:45:31Z | Merged: 2025-03-26T06:46:02Z

This PR introduces spend limit checks on batch proposals at construction and prior to signing. ~~This requires some changes to batch construction: the workers are now drained only _after_ the transmissions have been checked for inclusion in a batch. This, to avoid reinserting transmissions into the memory pool once the spend limit is surpassed.~~ I have modified the `Ready` queue to support `O(1)` operations at the front, as well as inclusion checks and regular inserts, so the batch proposal logic could be simplified. We no longer need to defer draining and can instead optimistically remove the transmission from the queue for processing. There's an occasional `O(n)` cost to reset the internally tracked offset but this should happen at most once per epoch, when solutions are cleared from the queue. It was also necessary to expose a `compute_cost` function on the `LedgerService` trait—its internals might still be moved into snarkVM. 

This changeset will need to be refined and tested, hence the draft. CI is currently expected to fail.

Related PRs: 
- https://github.com/ProvableHQ/snarkVM/pull/2601 (required for merge)
- https://github.com/ProvableHQ/snarkVM/pull/2565 (previous discussion)




Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 3ae4bdf8a4..bcc7d74e4b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1113,9 +1113,9 @@ dependencies = [
 
 [[package]]
 name = "fragile"
-version = "2.0.0"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
+checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
 
 [[package]]
 name = "fsio"
@@ -3663,6 +3663,7 @@ dependencies = [
 name = "snarkos-node-bft-ledger-service"
 version = "3.3.2"
 dependencies = [
+ "anyhow",
  "async-trait",
  "indexmap 2.8.0",
  "locktick",
@@ -3893,7 +3894,7 @@ dependencies = [
 [[package]]
 name = "snarkvm"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "anstyle",
  "anyhow",
@@ -3925,7 +3926,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-algorithms"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "anyhow",
@@ -3957,7 +3958,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-algorithms-cuda"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "blst",
  "cc",
@@ -3968,7 +3969,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-account",
  "snarkvm-circuit-algorithms",
@@ -3982,7 +3983,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-account"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-algorithms",
  "snarkvm-circuit-network",
@@ -3993,7 +3994,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-algorithms"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-types",
  "snarkvm-console-algorithms",
@@ -4003,7 +4004,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-collections"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-algorithms",
  "snarkvm-circuit-types",
@@ -4013,7 +4014,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-environment"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "indexmap 2.8.0",
  "itertools 0.11.0",
@@ -4032,12 +4033,12 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-environment-witness"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 
 [[package]]
 name = "snarkvm-circuit-network"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-algorithms",
  "snarkvm-circuit-collections",
@@ -4048,7 +4049,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-program"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "paste",
  "snarkvm-circuit-account",
@@ -4063,7 +4064,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-types"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-environment",
  "snarkvm-circuit-types-address",
@@ -4078,7 +4079,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-types-address"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-environment",
  "snarkvm-circuit-types-boolean",
@@ -4091,7 +4092,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-types-boolean"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-environment",
  "snarkvm-console-types-boolean",
@@ -4100,7 +4101,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-types-field"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-environment",
  "snarkvm-circuit-types-boolean",
@@ -4110,7 +4111,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-types-group"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-environment",
  "snarkvm-circuit-types-boolean",
@@ -4122,7 +4123,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-types-integers"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-environment",
  "snarkvm-circuit-types-boolean",
@@ -4134,7 +4135,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-types-scalar"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-environment",
  "snarkvm-circuit-types-boolean",
@@ -4145,7 +4146,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-circuit-types-string"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-circuit-environment",
  "snarkvm-circuit-types-boolean",
@@ -4157,7 +4158,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-console-account",
  "snarkvm-console-algorithms",
@@ -4170,7 +4171,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-account"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "bs58",
  "snarkvm-console-network",
@@ -4181,7 +4182,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-algorithms"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "blake2s_simd",
  "smallvec",
@@ -4194,7 +4195,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-collections"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "rayon",
@@ -4205,7 +4206,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-network"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "anyhow",
  "indexmap 2.8.0",
@@ -4228,7 +4229,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-network-environment"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "anyhow",
  "bech32",
@@ -4246,7 +4247,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-program"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "enum-iterator",
  "enum_index",
@@ -4268,7 +4269,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-types"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-console-network-environment",
  "snarkvm-console-types-address",
@@ -4283,7 +4284,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-types-address"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-console-network-environment",
  "snarkvm-console-types-boolean",
@@ -4294,7 +4295,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-types-boolean"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-console-network-environment",
 ]
@@ -4302,7 +4303,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-types-field"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-console-network-environment",
  "snarkvm-console-types-boolean",
@@ -4312,7 +4313,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-types-group"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-console-network-environment",
  "snarkvm-console-types-boolean",
@@ -4323,7 +4324,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-types-integers"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-console-network-environment",
  "snarkvm-console-types-boolean",
@@ -4334,7 +4335,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-types-scalar"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-console-network-environment",
  "snarkvm-console-types-boolean",
@@ -4345,7 +4346,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-console-types-string"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-console-network-environment",
  "snarkvm-console-types-boolean",
@@ -4356,7 +4357,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-curves"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "rand",
  "rayon",
@@ -4370,7 +4371,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-fields"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "anyhow",
@@ -4387,7 +4388,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "anyhow",
@@ -4414,7 +4415,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-authority"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "anyhow",
  "rand",
@@ -4426,7 +4427,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-block"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "indexmap 2.8.0",
  "rayon",
@@ -4446,7 +4447,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-committee"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "anyhow",
  "indexmap 2.8.0",
@@ -4465,7 +4466,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-narwhal"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-ledger-narwhal-batch-certificate",
  "snarkvm-ledger-narwhal-batch-header",
@@ -4478,7 +4479,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-narwhal-batch-certificate"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "indexmap 2.8.0",
  "rayon",
@@ -4491,7 +4492,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-narwhal-batch-header"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "indexmap 2.8.0",
  "rayon",
@@ -4504,7 +4505,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-narwhal-data"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "bytes",
  "serde_json",
@@ -4515,7 +4516,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-narwhal-subdag"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "indexmap 2.8.0",
  "rayon",
@@ -4530,7 +4531,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-narwhal-transmission"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "bytes",
  "serde_json",
@@ -4543,7 +4544,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-narwhal-transmission-id"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "snarkvm-console",
  "snarkvm-ledger-puzzle",
@@ -4552,7 +4553,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-puzzle"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "anyhow",
@@ -4573,7 +4574,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-puzzle-epoch"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "anyhow",
@@ -4595,7 +4596,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-query"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "async-trait",
  "reqwest 0.11.27",
@@ -4608,7 +4609,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-store"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std-storage",
  "anyhow",
@@ -4636,7 +4637,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-ledger-test-helpers"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "once_cell",
@@ -4652,7 +4653,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-metrics"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "metrics",
  "metrics-exporter-prometheus",
@@ -4661,7 +4662,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-parameters"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "anyhow",
@@ -4687,7 +4688,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-synthesizer"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "anyhow",
@@ -4720,7 +4721,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-synthesizer-process"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "colored",
@@ -4745,7 +4746,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-synthesizer-program"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "indexmap 2.8.0",
  "paste",
@@ -4760,7 +4761,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-synthesizer-snark"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "bincode",
  "once_cell",
@@ -4773,7 +4774,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-utilities"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "aleo-std",
  "anyhow",
@@ -4794,7 +4795,7 @@ dependencies = [
 [[package]]
 name = "snarkvm-utilities-derives"
 version = "1.3.0"
-source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=10bcf43#10bcf435d9308c0cfeae79ff7ebcf6418a1b7958"
+source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=7aaa46a#7aaa46ae2a01891f2d843e3413f2529688715118"
 dependencies = [
  "proc-macro2",
  "quote 1.0.40",
diff --git a/Cargo.toml b/Cargo.toml
index 80b46a5f09..071fa0023f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -47,7 +47,7 @@ default-features = false
 [workspace.dependencies.snarkvm] # If this is updated, the rev in `node/rest/Cargo.toml` must be updated as well.
 # path = "../snarkVM"
 git = "https://github.com/ProvableHQ/snarkVM.git"
-rev = "10bcf43"
+rev = "7aaa46a"
 #version = "=1.3.0"
 features = [ "circuit", "console", "rocks" ]
 
diff --git a/node/bft/ledger-service/Cargo.toml b/node/bft/ledger-service/Cargo.toml
index 57553dbc2b..cfa965621c 100644
--- a/node/bft/ledger-service/Cargo.toml
+++ b/node/bft/ledger-service/Cargo.toml
@@ -29,6 +29,9 @@ test = [ "mock", "translucent" ]
 translucent = [ "ledger" ]
 test_targets = [ "snarkvm/test_targets" ]
 
+[dependencies.anyhow]
+version = "1.0"
+
 [dependencies.async-trait]
 version = "0.1"
 
diff --git a/node/bft/ledger-service/src/ledger.rs b/node/bft/ledger-service/src/ledger.rs
index e9179eadeb..af0c44ed2e 100644
--- a/node/bft/ledger-service/src/ledger.rs
+++ b/node/bft/ledger-service/src/ledger.rs
@@ -23,9 +23,10 @@ use snarkvm::{
         puzzle::{Solution, SolutionID},
         store::ConsensusStorage,
     },
-    prelude::{Address, Field, FromBytes, Network, Result, bail, cfg_into_iter},
+    prelude::{Address, Field, FromBytes, Network, Result, bail, cfg_into_iter, deployment_cost, execution_cost_v2},
 };
 
+use anyhow::anyhow;
 use indexmap::IndexMap;
 #[cfg(feature = "locktick")]
 use locktick::parking_lot::RwLock;
@@ -313,15 +314,8 @@ impl<N: Network, C: ConsensusStorage<N>> LedgerService<N> for CoreLedgerService<
     async fn check_transaction_basic(
         &self,
         transaction_id: N::TransactionID,
-        transaction: Data<Transaction<N>>,
+        transaction: Transaction<N>,
     ) -> Result<()> {
-        // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
-        let transaction = spawn_blocking!({
-            match transaction {
-                Data::Object(transaction) => Ok(transaction),
-                Data::Buffer(bytes) => Ok(Transaction::<N>::read_le(&mut bytes.take(N::MAX_TRANSACTION_SIZE as u64))?),
-            }
-        })?;
         // Ensure the transaction ID matches in the transaction.
         if transaction_id != transaction.id() {
             bail!("Invalid transaction - expected {transaction_id}, found {}", transaction.id());
@@ -384,4 +378,30 @@ impl<N: Network, C: ConsensusStorage<N>> LedgerService<N> for CoreLedgerService<
         tracing::info!("\n\nAdvanced to block {} at round {} - {}\n", block.height(), block.round(), block.hash());
         Ok(())
     }
+
+    /// Returns the spent cost for a transaction in microcredits.
+    /// This is used to limit the amount of compute in the block generation hot
+    /// path. This does NOT represent the full costs which a user has to pay.
+    fn transaction_spent_cost_in_microcredits(
+        &self,
+        _transaction_id: N::TransactionID,
+        transaction: Transaction<N>,
+    ) -> Result<u64> {
+        match &transaction {
+            // Include the synthesis cost and storage cost for deployments.
+            Transaction::Deploy(_, _, _, deployment, _) => {
+                let (_, (storage_cost, synthesis_cost, _)) = deployment_cost(deployment)?;
+                storage_cost
+                    .checked_add(synthesis_cost)
+                    .ok_or(anyhow!("The storage and synthesis cost computation overflowed for a deployment"))
+            }
+            // Include the finalize cost and storage cost for executions.
+            Transaction::Execute(_, _, execution, _) => {
+                let (total_cost, (_, _)) = execution_cost_v2(&self.ledger.vm().process().read(), execution)?;
+                Ok(total_cost)
+            }
+            // Fee transactions are internal to the VM, they do not have a compute cost.
+            Transaction::Fee(..) => Ok(0),
+        }
+    }
 }
diff --git a/node/bft/ledger-service/src/mock.rs b/node/bft/ledger-service/src/mock.rs
index 0763ffe776..b0561697b6 100644
--- a/node/bft/ledger-service/src/mock.rs
+++ b/node/bft/ledger-service/src/mock.rs
@@ -205,7 +205,7 @@ impl<N: Network> LedgerService<N> for MockLedgerService<N> {
     async fn check_transaction_basic(
         &self,
         transaction_id: N::TransactionID,
-        _transaction: Data<Transaction<N>>,
+        _transaction: Transaction<N>,
     ) -> Result<()> {
         trace!("[MockLedgerService] Check transaction basic {:?} - Ok", fmt_id(transaction_id));
         Ok(())
@@ -238,4 +238,13 @@ impl<N: Network> LedgerService<N> for MockLedgerService<N> {
         self.height_to_round_and_hash.lock().insert(block.height(), (block.round(), block.hash()));
         Ok(())
     }
+
+    /// Returns the spent cost for a transaction in microcredits.
+    fn transaction_spent_cost_in_microcredits(
+        &self,
+        _transaction_id: N::TransactionID,
+        _transaction: Transaction<N>,
+    ) -> Result<u64> {
+        Ok(N::TRANSACTION_SPEND_LIMIT)
+    }
 }
diff --git a/node/bft/ledger-service/src/prover.rs b/node/bft/ledger-service/src/prover.rs
index 8bb447f5fc..44dcc5d2e8 100644
--- a/node/bft/ledger-service/src/prover.rs
+++ b/node/bft/ledger-service/src/prover.rs
@@ -161,7 +161,7 @@ impl<N: Network> LedgerService<N> for ProverLedgerService<N> {
     async fn check_transaction_basic(
         &self,
         _transaction_id: N::TransactionID,
-        _transaction: Data<Transaction<N>>,
+        _transaction: Transaction<N>,
     ) -> Result<()> {
         Ok(())
     }
@@ -186,4 +186,13 @@ impl<N: Network> LedgerService<N> for ProverLedgerService<N> {
     fn advance_to_next_block(&self, block: &Block<N>) -> Result<()> {
         bail!("Cannot advance to next block in prover - {block}")
     }
+
+    /// Returns the spent cost for a transaction in microcredits.
+    fn transaction_spent_cost_in_microcredits(
+        &self,
+        transaction_id: N::TransactionID,
+        _transaction: Transaction<N>,
+    ) -> Result<u64> {
+        bail!("Transaction '{transaction_id}' doesn't exist in prover")
+    }
 }
diff --git a/node/bft/ledger-service/src/traits.rs b/node/bft/ledger-service/src/traits.rs
index c0782875d9..16aa9b93ee 100644
--- a/node/bft/ledger-service/src/traits.rs
+++ b/node/bft/ledger-service/src/traits.rs
@@ -103,7 +103,7 @@ pub trait LedgerService<N: Network>: Debug + Send + Sync {
     async fn check_transaction_basic(
         &self,
         transaction_id: N::TransactionID,
-        transaction: Data<Transaction<N>>,
+        transaction: Transaction<N>,
     ) -> Result<()>;
 
     /// Checks the given block is valid next block.
@@ -120,4 +120,11 @@ pub trait LedgerService<N: Network>: Debug + Send + Sync {
     /// Adds the given block as the next block in the ledger.
     #[cfg(feature = "ledger-write")]
     fn advance_to_next_block(&self, block: &Block<N>) -> Result<()>;
+
+    /// Returns the spent cost for a transaction in microcredits.
+    fn transaction_spent_cost_in_microcredits(
+        &self,
+        transaction_id: N::TransactionID,
+        transaction: Transaction<N>,
+    ) -> Result<u64>;
 }
diff --git a/node/bft/ledger-service/src/translucent.rs b/node/bft/ledger-service/src/translucent.rs
index 3d0b17b1ee..994481bd7a 100644
--- a/node/bft/ledger-service/src/translucent.rs
+++ b/node/bft/ledger-service/src/translucent.rs
@@ -172,7 +172,7 @@ impl<N: Network, C: ConsensusStorage<N>> LedgerService<N> for TranslucentLedgerS
     async fn check_transaction_basic(
         &self,
         _transaction_id: N::TransactionID,
-        _transaction: Data<Transaction<N>>,
+        _transaction: Transaction<N>,
     ) -> Result<()> {
         Ok(())
     }
@@ -195,4 +195,13 @@ impl<N: Network, C: ConsensusStorage<N>> LedgerService<N> for TranslucentLedgerS
     fn advance_to_next_block(&self, block: &Block<N>) -> Result<()> {
         self.inner.advance_to_next_block(block)
     }
+
+    /// Returns the spent cost for a transaction in microcredits.
+    fn transaction_spent_cost_in_microcredits(
+        &self,
+        transaction_id: N::TransactionID,
+        transaction: Transaction<N>,
+    ) -> Result<u64> {
+        self.inner.transaction_spent_cost_in_microcredits(transaction_id, transaction)
+    }
 }
diff --git a/node/bft/src/helpers/ready.rs b/node/bft/src/helpers/ready.rs
index 19aa20a0a7..3e4dfd7b06 100644
--- a/node/bft/src/helpers/ready.rs
+++ b/node/bft/src/helpers/ready.rs
@@ -23,16 +23,18 @@ use snarkvm::{
 };
 
 use indexmap::{IndexMap, IndexSet};
-#[cfg(feature = "locktick")]
-use locktick::parking_lot::RwLock;
-#[cfg(not(feature = "locktick"))]
-use parking_lot::RwLock;
-use std::sync::Arc;
+use std::collections::{HashMap, VecDeque, hash_map::Entry::Vacant};
 
 #[derive(Clone, Debug)]
 pub struct Ready<N: Network> {
-    /// The current map of `(transmission ID, transmission)` entries.
-    transmissions: Arc<RwLock<IndexMap<TransmissionID<N>, Transmission<N>>>>,
+    /// Maps each transmission ID to its logical index (physical index + offset)
+    /// in `transmissions`.
+    transmission_ids: HashMap<TransmissionID<N>, i64>,
+    /// An ordered collection of (transmission ID, transmission).
+    transmissions: VecDeque<(TransmissionID<N>, Transmission<N>)>,
+    /// An offset used to adjust logical indices when elements are inserted or
+    /// removed at the front.
+    offset: i64,
 }
 
 impl<N: Network> Default for Ready<N> {
@@ -45,105 +47,152 @@ impl<N: Network> Default for Ready<N> {
 impl<N: Network> Ready<N> {
     /// Initializes a new instance of the ready queue.
     pub fn new() -> Self {
-        Self { transmissions: Default::default() }
+        Self { transmission_ids: Default::default(), transmissions: Default::default(), offset: Default::default() }
     }
 
     /// Returns `true` if the ready queue is empty.
     pub fn is_empty(&self) -> bool {
-        self.transmissions.read().is_empty()
+        self.transmissions.is_empty()
     }
 
     /// Returns the number of transmissions in the ready queue.
     pub fn num_transmissions(&self) -> usize {
-        self.transmissions.read().len()
+        self.transmissions.len()
     }
 
     /// Returns the number of ratifications in the ready queue.
     pub fn num_ratifications(&self) -> usize {
-        self.transmissions.read().keys().filter(|id| matches!(id, TransmissionID::Ratification)).count()
+        self.transmission_ids.keys().filter(|id| matches!(id, TransmissionID::Ratification)).count()
     }
 
     /// Returns the number of solutions in the ready queue.
     pub fn num_solutions(&self) -> usize {
-        self.transmissions.read().keys().filter(|id| matches!(id, TransmissionID::Solution(..))).count()
+        self.transmission_ids.keys().filter(|id| matches!(id, TransmissionID::Solution(..))).count()
     }
 
     /// Returns the number of transactions in the ready queue.
     pub fn num_transactions(&self) -> usize {
-        self.transmissions.read().keys().filter(|id| matches!(id, TransmissionID::Transaction(..))).count()
+        self.transmission_ids.keys().filter(|id| matches!(id, TransmissionID::Transaction(..))).count()
     }
 
     /// Returns the transmission IDs in the ready queue.
     pub fn transmission_ids(&self) -> IndexSet<TransmissionID<N>> {
-        self.transmissions.read().keys().copied().collect()
+        self.transmission_ids.keys().copied().collect()
     }
 
     /// Returns the transmissions in the ready queue.
     pub fn transmissions(&self) -> IndexMap<TransmissionID<N>, Transmission<N>> {
-        self.transmissions.read().clone()
+        self.transmissions.iter().cloned().collect()
     }
 
     /// Returns the solutions in the ready queue.
-    pub fn solutions(&self) -> impl '_ + Iterator<Item = (SolutionID<N>, Data<Solution<N>>)> {
-        self.transmissions.read().clone().into_iter().filter_map(|(id, transmission)| match (id, transmission) {
-            (TransmissionID::Solution(id, _), Transmission::Solution(solution)) => Some((id, solution)),
-            _ => None,
-        })
+    pub fn solutions(&self) -> Vec<(SolutionID<N>, Data<Solution<N>>)> {
+        self.transmissions
+            .iter()
+            .filter_map(|(id, transmission)| match (id, transmission) {
+                (TransmissionID::Solution(id, _), Transmission::Solution(solution)) => Some((*id, solution.clone())),
+                _ => None,
+            })
+            .collect()
     }
 
     /// Returns the transactions in the ready queue.
-    pub fn transactions(&self) -> impl '_ + Iterator<Item = (N::TransactionID, Data<Transaction<N>>)> {
-        self.transmissions.read().clone().into_iter().filter_map(|(id, transmission)| match (id, transmission) {
-            (TransmissionID::Transaction(id, _), Transmission::Transaction(tx)) => Some((id, tx)),
-            _ => None,
-        })
+    pub fn transactions(&self) -> Vec<(N::TransactionID, Data<Transaction<N>>)> {
+        self.transmissions
+            .iter()
+            .filter_map(|(id, transmission)| match (id, transmission) {
+                (TransmissionID::Transaction(id, _), Transmission::Transaction(tx)) => Some((*id, tx.clone())),
+                _ => None,
+            })
+            .collect()
     }
 }
 
 impl<N: Network> Ready<N> {
     /// Returns `true` if the ready queue contains the specified `transmission ID`.
     pub fn contains(&self, transmission_id: impl Into<TransmissionID<N>>) -> bool {
-        self.transmissions.read().contains_key(&transmission_id.into())
+        self.transmission_ids.contains_key(&transmission_id.into())
     }
 
     /// Returns the transmission, given the specified `transmission ID`.
     pub fn get(&self, transmission_id: impl Into<TransmissionID<N>>) -> Option<Transmission<N>> {
-        self.transmissions.read().get(&transmission_id.into()).cloned()
+        self.transmission_ids
+            .get(&transmission_id.into())
+            .and_then(|&index| self.transmissions.get((index - self.offset) as usize))
+            .map(|(_, transmission)| transmission.clone())
     }
 
     /// Inserts the specified (`transmission ID`, `transmission`) to the ready queue.
     /// Returns `true` if the transmission is new, and was added to the ready queue.
-    pub fn insert(&self, transmission_id: impl Into<TransmissionID<N>>, transmission: Transmission<N>) -> bool {
+    pub fn insert(&mut self, transmission_id: impl Into<TransmissionID<N>>, transmission: Transmission<N>) -> bool {
+        let physical_index = self.transmissions.len();
         let transmission_id = transmission_id.into();
-        // Insert the transmission ID.
-        let is_new = self.transmissions.write().insert(transmission_id, transmission).is_none();
-        // Return whether the transmission is new.
-        is_new
+
+        if let Vacant(entry) = self.transmission_ids.entry(transmission_id) {
+            entry.insert(physical_index as i64 + self.offset);
+            self.transmissions.push_back((transmission_id, transmission));
+            true
+        } else {
+            false
+        }
+    }
+
+    /// Inserts the specified (`transmission ID`, `transmission`) at the front
+    /// of the ready queue.
+    /// Returns `true` if the transmission is new, and was added to the ready queue.
+    pub fn insert_front(
+        &mut self,
+        transmission_id: impl Into<TransmissionID<N>>,
+        transmission: Transmission<N>,
+    ) -> bool {
+        let transmission_id = transmission_id.into();
+        if let Vacant(entry) = self.transmission_ids.entry(transmission_id) {
+            self.offset -= 1;
+            let index = self.offset;
+
+            entry.insert(index);
+            self.transmissions.push_front((transmission_id, transmission));
+            true
+        } else {
+            false
+        }
     }
 
-    /// Removes up to the specified number of transmissions and returns them.
-    pub fn drain(&self, num_transmissions: usize) -> IndexMap<TransmissionID<N>, Transmission<N>> {
-        // Acquire the write lock.
-        let mut transmissions = self.transmissions.write();
-        // Determine the number of transmissions to drain.
-        let range = 0..transmissions.len().min(num_transmissions);
-        // Drain the transmission IDs.
-        transmissions.drain(range).collect::<IndexMap<_, _>>()
+    /// Removes and returns the transmission at the front of the queue.
+    pub fn remove_front(&mut self) -> Option<(TransmissionID<N>, Transmission<N>)> {
+        if let Some((transmission_id, transmission)) = self.transmissions.pop_front() {
+            self.transmission_ids.remove(&transmission_id);
+
+            if self.transmission_ids.is_empty() {
+                debug_assert!(self.transmissions.is_empty());
+                self.offset = 0;
+            } else {
+                self.offset += 1;
+            }
+
+            Some((transmission_id, transmission))
+        } else {
+            None
+        }
     }
 
-    /// Clears all solutions from the ready queue.
-    pub(crate) fn clear_solutions(&self) {
-        // Acquire the write lock.
-        let mut transmissions = self.transmissions.write();
-        // Remove all solutions.
-        transmissions.retain(|id, _| !matches!(id, TransmissionID::Solution(..)));
+    /// Removes all solution transmissions from the queue (O(n)).
+    pub fn clear_solutions(&mut self) {
+        self.transmissions.retain(|(_, transmission)| !matches!(transmission, Transmission::Solution(_)));
+
+        // Rebuild the index and reset the offset.
+        self.transmission_ids.clear();
+        self.offset = 0;
+        for (i, (id, _)) in self.transmissions.iter().enumerate() {
+            self.transmission_ids.insert(*id, i as i64);
+        }
     }
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use snarkvm::ledger::narwhal::Data;
+    use snarkvm::{ledger::narwhal::Data, prelude::Field};
 
     use ::bytes::Bytes;
 
@@ -157,7 +206,7 @@ mod tests {
         let data = |rng: &mut TestRng| Data::Buffer(Bytes::from((0..512).map(|_| rng.gen::<u8>()).collect::<Vec<_>>()));
 
         // Initialize the ready queue.
-        let ready = Ready::<CurrentNetwork>::new();
+        let mut ready = Ready::<CurrentNetwork>::new();
 
         // Initialize the solution IDs.
         let solution_id_1 = TransmissionID::Solution(
@@ -205,20 +254,21 @@ mod tests {
         assert_eq!(ready.get(solution_id_unknown), None);
 
         // Drain the ready queue.
-        let transmissions = ready.drain(3);
+        let mut transmissions = Vec::with_capacity(3);
+        for _ in 0..3 {
+            transmissions.push(ready.remove_front().unwrap())
+        }
 
         // Check the number of transmissions.
         assert!(ready.is_empty());
         // Check the transmission IDs.
         assert_eq!(ready.transmission_ids(), IndexSet::new());
-
         // Check the transmissions.
-        assert_eq!(
-            transmissions,
-            vec![(solution_id_1, solution_1), (solution_id_2, solution_2), (solution_id_3, solution_3)]
-                .into_iter()
-                .collect::<IndexMap<_, _>>()
-        );
+        assert_eq!(transmissions, vec![
+            (solution_id_1, solution_1),
+            (solution_id_2, solution_2),
+            (solution_id_3, solution_3)
+        ]);
     }
 
     #[test]
@@ -232,7 +282,7 @@ mod tests {
         let data = Data::Buffer(Bytes::from(vec));
 
         // Initialize the ready queue.
-        let ready = Ready::<CurrentNetwork>::new();
+        let mut ready = Ready::<CurrentNetwork>::new();
 
         // Initialize the solution ID.
         let solution_id = TransmissionID::Solution(
@@ -250,4 +300,97 @@ mod tests {
         // Check the number of transmissions.
         assert_eq!(ready.num_transmissions(), 1);
     }
+
+    #[test]
+    fn test_insert_front() {
+        let rng = &mut TestRng::default();
+        let data = |rng: &mut TestRng| Data::Buffer(Bytes::from((0..512).map(|_| rng.gen::<u8>()).collect::<Vec<_>>()));
+
+        // Initialize the ready queue.
+        let mut ready = Ready::<CurrentNetwork>::new();
+
+        // Initialize the solution IDs.
+        let solution_id_1 = TransmissionID::Solution(
+            rng.gen::<u64>().into(),
+            rng.gen::<<CurrentNetwork as Network>::TransmissionChecksum>(),
+        );
+        let solution_id_2 = TransmissionID::Solution(
+            rng.gen::<u64>().into(),
+            rng.gen::<<CurrentNetwork as Network>::TransmissionChecksum>(),
+        );
+
+        // Initialize the solutions.
+        let solution_1 = Transmission::Solution(data(rng));
+        let solution_2 = Transmission::Solution(data(rng));
+
+        // Insert the two solutions at the front, check the offset.
+        assert!(ready.insert_front(solution_id_1, solution_1.clone()));
+        assert_eq!(ready.offset, -1);
+        assert!(ready.insert_front(solution_id_2, solution_2.clone()));
+        assert_eq!(ready.offset, -2);
+
+        // Check retrieval.
+        assert_eq!(ready.get(solution_id_1), Some(solution_1.clone()));
+        assert_eq!(ready.get(solution_id_2), Some(solution_2.clone()));
+
+        // Remove from the front, offset should have increased by 1.
+        let removed_solution = ready.remove_front().unwrap();
+        assert_eq!(removed_solution, (solution_id_2, solution_2));
+        assert_eq!(ready.offset, -1);
+
+        // Remove another transmission from the front, the offset should be back to 0.
+        let removed_solution = ready.remove_front().unwrap();
+        assert_eq!(removed_solution, (solution_id_1, solution_1));
+        assert_eq!(ready.offset, 0);
+    }
+
+    #[test]
+    fn test_clear_solutions() {
+        let rng = &mut TestRng::default();
+        let solution_data =
+            |rng: &mut TestRng| Data::Buffer(Bytes::from((0..512).map(|_| rng.gen::<u8>()).collect::<Vec<_>>()));
+        let transaction_data =
+            |rng: &mut TestRng| Data::Buffer(Bytes::from((0..512).map(|_| rng.gen::<u8>()).collect::<Vec<_>>()));
+
+        // Initialize the ready queue.
+        let mut ready = Ready::<CurrentNetwork>::new();
+
+        // Initialize the solution IDs.
+        let solution_id_1 = TransmissionID::Solution(
+            rng.gen::<u64>().into(),
+            rng.gen::<<CurrentNetwork as Network>::TransmissionChecksum>(),
+        );
+        let solution_id_2 = TransmissionID::Solution(
+            rng.gen::<u64>().into(),
+            rng.gen::<<CurrentNetwork as Network>::TransmissionChecksum>(),
+        );
+        let transaction_id = TransmissionID::Transaction(
+            <CurrentNetwork as Network>::TransactionID::from(Field::rand(rng)),
+            <CurrentNetwork as Network>::TransmissionChecksum::from(rng.gen::<u128>()),
+        );
+
+        // Initialize the transmissions.
+        let solution_1 = Transmission::Solution(solution_data(rng));
+        let solution_2 = Transmission::Solution(solution_data(rng));
+        let transaction = Transmission::Transaction(transaction_data(rng));
+
+        // Insert the solution, check the offset should be decremented.
+        assert!(ready.insert_front(solution_id_1, solution_1.clone()));
+        assert_eq!(ready.offset, -1);
+
+        // Insert the transaction and the second solution, the offset should remain unchanged.
+        assert!(ready.insert(transaction_id, transaction.clone()));
+        assert_eq!(ready.offset, -1);
+        assert!(ready.insert(solution_id_2, solution_2.clone()));
+        assert_eq!(ready.offset, -1);
+
+        // Clear all solution transmissions.
+        ready.clear_solutions();
+        // Only the transaction should remain.
+        assert_eq!(ready.num_transmissions(), 1);
+        // The offset should now be reset to 0.
+        assert_eq!(ready.offset, 0);
+        // The remaining transmission is the transaction.
+        assert_eq!(ready.get(transaction_id), Some(transaction));
+    }
 }
diff --git a/node/bft/src/primary.rs b/node/bft/src/primary.rs
index db6364e993..2f95e9669e 100644
--- a/node/bft/src/primary.rs
+++ b/node/bft/src/primary.rs
@@ -493,90 +493,131 @@ impl<N: Network> Primary<N> {
             return Ok(());
         }
 
-        // Determined the required number of transmissions per worker.
-        let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
-        // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
-            // Initialize a tracker for included transmissions for the current worker.
-            let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
+        // Note: worker draining and transaction inclusion needs to be thought
+        // through carefully when there is more than one worker. The fairness
+        // provided by one worker (FIFO) is no longer guaranteed with multiple workers.
+        debug_assert_eq!(MAX_WORKERS, 1);
+
+        'outer: for worker in self.workers().iter() {
+            let mut num_worker_transmissions = 0usize;
+
+            while let Some((id, transmission)) = worker.remove_front() {
+                // Check the selected transmissions are below the batch limit.
+                if transmissions.len() >= BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
                     break 'outer;
                 }
-                // Iterate through the worker transmissions.
-                'inner: for (id, transmission) in worker_transmissions {
-                    // Check if the ledger already contains the transmission.
-                    if self.ledger.contains_transmission(&id).unwrap_or(true) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
-                        continue 'inner;
-                    }
-                    // Check if the storage already contain the transmission.
-                    // Note: We do not skip if this is the first transmission in the proposal, to ensure that
-                    // the primary does not propose a batch with no transmissions.
-                    if !transmissions.is_empty() && self.storage.contains_transmission(id) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
-                        continue 'inner;
+
+                // Check the max transmissions per worker is not exceeded.
+                if num_worker_transmissions >= Worker::<N>::MAX_TRANSMISSIONS_PER_WORKER {
+                    continue 'outer;
+                }
+
+                // Check if the ledger already contains the transmission.
+                if self.ledger.contains_transmission(&id).unwrap_or(true) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
+                    continue;
+                }
+
+                // Check if the storage already contain the transmission.
+                // Note: We do not skip if this is the first transmission in the proposal, to ensure that
+                // the primary does not propose a batch with no transmissions.
+                if !transmissions.is_empty() && self.storage.contains_transmission(id) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
+                    continue;
+                }
+
+                // Check the transmission is still valid.
+                match (id, transmission.clone()) {
+                    (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
+                        // Ensure the checksum matches. If not, skip the solution.
+                        if !matches!(solution.to_checksum::<N>(), Ok(solution_checksum) if solution_checksum == checksum)
+                        {
+                            trace!("Proposing - Skipping solution '{}' - Checksum mismatch", fmt_id(solution_id));
+                            continue;
+                        }
+                        // Check if the solution is still valid.
+                        if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
+                            trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
+                            continue;
+                        }
                     }
-                    // Check the transmission is still valid.
-                    match (id, transmission.clone()) {
-                        (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
-                            // Ensure the checksum matches.
-                            match solution.to_checksum::<N>() {
-                                Ok(solution_checksum) if solution_checksum == checksum => (),
-                                _ => {
-                                    trace!(
-                                        "Proposing - Skipping solution '{}' - Checksum mismatch",
-                                        fmt_id(solution_id)
-                                    );
-                                    continue 'inner;
-                                }
-                            }
-                            // Check if the solution is still valid.
-                            if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
-                                trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
-                                continue 'inner;
-                            }
+                    (TransmissionID::Transaction(transaction_id, checksum), Transmission::Transaction(transaction)) => {
+                        // Ensure the checksum matches. If not, skip the transaction.
+                        if !matches!(transaction.to_checksum::<N>(), Ok(transaction_checksum) if transaction_checksum == checksum )
+                        {
+                            trace!("Proposing - Skipping transaction '{}' - Checksum mismatch", fmt_id(transaction_id));
+                            continue;
                         }
-                        (
-                            TransmissionID::Transaction(transaction_id, checksum),
-                            Transmission::Transaction(transaction),
-                        ) => {
-                            // Ensure the checksum matches.
-                            match transaction.to_checksum::<N>() {
-                                Ok(transaction_checksum) if transaction_checksum == checksum => (),
-                                _ => {
-                                    trace!(
-                                        "Proposing - Skipping transaction '{}' - Checksum mismatch",
-                                        fmt_id(transaction_id)
-                                    );
-                                    continue 'inner;
+
+                        // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
+                        let transaction = spawn_blocking!({
+                            match transaction {
+                                Data::Object(transaction) => Ok(transaction),
+                                Data::Buffer(bytes) => {
+                                    Ok(Transaction::<N>::read_le(&mut bytes.take(N::MAX_TRANSACTION_SIZE as u64))?)
                                 }
                             }
-                            // Check if the transaction is still valid.
-                            if let Err(e) = self.ledger.check_transaction_basic(transaction_id, transaction).await {
-                                trace!("Proposing - Skipping transaction '{}' - {e}", fmt_id(transaction_id));
-                                continue 'inner;
-                            }
+                        })?;
+
+                        // Check if the transaction is still valid.
+                        // TODO: check if clone is cheap, otherwise fix.
+                        if let Err(e) = self.ledger.check_transaction_basic(transaction_id, transaction.clone()).await {
+                            trace!("Proposing - Skipping transaction '{}' - {e}", fmt_id(transaction_id));
+                            continue;
                         }
-                        // Note: We explicitly forbid including ratifications,
-                        // as the protocol currently does not support ratifications.
-                        (TransmissionID::Ratification, Transmission::Ratification) => continue,
-                        // All other combinations are clearly invalid.
-                        _ => continue 'inner,
+
+                        // Compute the transaction spent cost (in microcredits).
+                        // Note: We purposefully discard this transaction if we are unable to compute the spent cost.
+                        let Ok(cost) = self.ledger.transaction_spent_cost_in_microcredits(transaction_id, transaction)
+                        else {
+                            debug!(
+                                "Proposing - Skipping and discarding transaction '{}' - Unable to compute transaction spent cost",
+                                fmt_id(transaction_id)
+                            );
+                            continue;
+                        };
+
+                        // Compute the next proposal cost.
+                        // Note: We purposefully discard this transaction if the proposal cost overflows.
+                        let Some(next_proposal_cost) = proposal_cost.checked_add(cost) else {
+                            debug!(
+                                "Proposing - Skipping and discarding transaction '{}' - Proposal cost overflowed",
+                                fmt_id(transaction_id)
+                            );
+                            continue;
+                        };
+
+                        // Check if the next proposal cost exceeds the batch proposal spend limit.
+                        if next_proposal_cost > BatchHeader::<N>::BATCH_SPEND_LIMIT {
+                            trace!(
+                                "Proposing - Skipping transaction '{}' - Batch spend limit surpassed ({next_proposal_cost} > {})",
+                                fmt_id(transaction_id),
+                                BatchHeader::<N>::BATCH_SPEND_LIMIT
+                            );
+
+                            // Reinsert the transmission into the worker.
+                            worker.insert_front(id, transmission);
+                            break 'outer;
+                        }
+
+                        // Update the proposal cost.
+                        proposal_cost = next_proposal_cost;
                     }
-                    // Insert the transmission into the map.
-                    transmissions.insert(id, transmission);
-                    num_transmissions_included_for_worker += 1;
+
+                    // Note: We explicitly forbid including ratifications,
+                    // as the protocol currently does not support ratifications.
+                    (TransmissionID::Ratification, Transmission::Ratification) => continue,
+                    // All other combinations are clearly invalid.
+                    _ => continue,
                 }
+
+                // If the transmission is valid, insert it into the proposal's transmission list.
+                transmissions.insert(id, transmission);
+                num_worker_transmissions = num_worker_transmissions.saturating_add(1);
             }
         }
 
@@ -773,6 +814,70 @@ impl<N: Network> Primary<N> {
         // Inserts the missing transmissions into the workers.
         self.insert_missing_transmissions_into_workers(peer_ip, missing_transmissions.into_iter())?;
 
+        // Ensure the transaction doesn't bring the proposal above the spend limit.
+        let block_height = self.ledger.latest_block_height() + 1;
+        if N::CONSENSUS_VERSION(block_height)? >= ConsensusVersion::V4 {
+            let mut proposal_cost = 0u64;
+            for transmission_id in batch_header.transmission_ids() {
+                let worker_id = assign_to_worker(*transmission_id, self.num_workers())?;
+                let Some(worker) = self.workers.get(worker_id as usize) else {
+                    debug!("Unable to find worker {worker_id}");
+                    return Ok(());
+                };
+
+                let Some(transmission) = worker.get_transmission(*transmission_id) else {
+                    debug!("Unable to find transmission '{}' in worker '{worker_id}", fmt_id(transmission_id));
+                    return Ok(());
+                };
+
+                // If the transmission is a transaction, compute its execution cost.
+                if let (TransmissionID::Transaction(transaction_id, _), Transmission::Transaction(transaction)) =
+                    (transmission_id, transmission)
+                {
+                    // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
+                    let transaction = spawn_blocking!({
+                        match transaction {
+                            Data::Object(transaction) => Ok(transaction),
+                            Data::Buffer(bytes) => {
+                                Ok(Transaction::<N>::read_le(&mut bytes.take(N::MAX_TRANSACTION_SIZE as u64))?)
+                            }
+                        }
+                    })?;
+
+                    // Compute the transaction spent cost (in microcredits).
+                    // Note: We purposefully discard this transaction if we are unable to compute the spent cost.
+                    let Ok(cost) = self.ledger.transaction_spent_cost_in_microcredits(*transaction_id, transaction)
+                    else {
+                        bail!(
+                            "Invalid batch proposal - Unable to compute transaction spent cost on transaction '{}'",
+                            fmt_id(transaction_id)
+                        )
+                    };
+
+                    // Compute the next proposal cost.
+                    // Note: We purposefully discard this transaction if the proposal cost overflows.
+                    let Some(next_proposal_cost) = proposal_cost.checked_add(cost) else {
+                        bail!(
+                            "Invalid batch proposal - Batch proposal overflowed on transaction '{}'",
+                            fmt_id(transaction_id)
+                        )
+                    };
+
+                    // Check if the next proposal cost exceeds the batch proposal spend limit.
+                    if next_proposal_cost > BatchHeader::<N>::BATCH_SPEND_LIMIT {
+                        bail!(
+                            "Malicious peer - Batch proposal from '{peer_ip}' exceeds the spend limit on transaction '{}' ({next_proposal_cost} > {})",
+                            fmt_id(transaction_id),
+                            BatchHeader::<N>::BATCH_SPEND_LIMIT
+                        );
+                    }
+
+                    // Update the proposal cost.
+                    proposal_cost = next_proposal_cost;
+                }
+            }
+        }
+
         /* Proceeding to sign the batch. */
 
         // Retrieve the batch ID.
@@ -814,6 +919,7 @@ impl<N: Network> Primary<N> {
                 debug!("Signed a batch for round {batch_round} from '{peer_ip}'");
             }
         });
+
         Ok(())
     }
 
@@ -1772,7 +1878,10 @@ mod tests {
     use snarkos_node_bft_ledger_service::MockLedgerService;
     use snarkos_node_bft_storage_service::BFTMemoryService;
     use snarkvm::{
-        ledger::committee::{Committee, MIN_VALIDATOR_STAKE},
+        ledger::{
+            committee::{Committee, MIN_VALIDATOR_STAKE},
+            ledger_test_helpers::sample_execution_transaction_with_fee,
+        },
         prelude::{Address, Signature},
     };
 
@@ -1782,31 +1891,35 @@ mod tests {
 
     type CurrentNetwork = snarkvm::prelude::MainnetV0;
 
-    // Returns a primary and a list of accounts in the configured committee.
-    async fn primary_without_handlers(
-        rng: &mut TestRng,
-    ) -> (Primary<CurrentNetwork>, Vec<(SocketAddr, Account<CurrentNetwork>)>) {
+    fn sample_committee(rng: &mut TestRng) -> (Vec<(SocketAddr, Account<CurrentNetwork>)>, Committee<CurrentNetwork>) {
         // Create a committee containing the primary's account.
-        let (accounts, committee) = {
-            const COMMITTEE_SIZE: usize = 4;
-            let mut accounts = Vec::with_capacity(COMMITTEE_SIZE);
-            let mut members = IndexMap::new();
-
-            for i in 0..COMMITTEE_SIZE {
-                let socket_addr = format!("127.0.0.1:{}", 5000 + i).parse().unwrap();
-                let account = Account::new(rng).unwrap();
-                members.insert(account.address(), (MIN_VALIDATOR_STAKE, true, rng.gen_range(0..100)));
-                accounts.push((socket_addr, account));
-            }
+        const COMMITTEE_SIZE: usize = 4;
+        let mut accounts = Vec::with_capacity(COMMITTEE_SIZE);
+        let mut members = IndexMap::new();
 
-            (accounts, Committee::<CurrentNetwork>::new(1, members).unwrap())
-        };
+        for i in 0..COMMITTEE_SIZE {
+            let socket_addr = format!("127.0.0.1:{}", 5000 + i).parse().unwrap();
+            let account = Account::new(rng).unwrap();
+
+            members.insert(account.address(), (MIN_VALIDATOR_STAKE, true, rng.gen_range(0..100)));
+            accounts.push((socket_addr, account));
+        }
+
+        (accounts, Committee::<CurrentNetwork>::new(1, members).unwrap())
+    }
 
-        let account = accounts.first().unwrap().1.clone();
-        let ledger = Arc::new(MockLedgerService::new(committee));
+    // Returns a primary and a list of accounts in the configured committee.
+    fn primary_with_committee(
+        account_index: usize,
+        accounts: &[(SocketAddr, Account<CurrentNetwork>)],
+        committee: Committee<CurrentNetwork>,
+        height: u32,
+    ) -> Primary<CurrentNetwork> {
+        let ledger = Arc::new(MockLedgerService::new_at_height(committee, height));
         let storage = Storage::new(ledger.clone(), Arc::new(BFTMemoryService::new()), 10);
 
         // Initialize the primary.
+        let account = accounts[account_index].1.clone();
         let mut primary = Primary::new(account, storage, ledger, None, &[], None).unwrap();
 
         // Construct a worker instance.
@@ -1818,10 +1931,24 @@ mod tests {
             primary.proposed_batch.clone(),
         )
         .unwrap()]);
-        for a in accounts.iter() {
+        for a in accounts.iter().skip(account_index) {
             primary.gateway.insert_connected_peer(a.0, a.0, a.1.address());
         }
 
+        primary
+    }
+
+    fn primary_without_handlers(
+        rng: &mut TestRng,
+    ) -> (Primary<CurrentNetwork>, Vec<(SocketAddr, Account<CurrentNetwork>)>) {
+        let (accounts, committee) = sample_committee(rng);
+        let primary = primary_with_committee(
+            0, // index of primary's account
+            &accounts,
+            committee,
+            CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V1).unwrap(),
+        );
+
         (primary, accounts)
     }
 
@@ -1839,20 +1966,14 @@ mod tests {
         (solution_id, solution)
     }
 
-    // Creates a mock transaction.
+    // Samples a test transaction.
     fn sample_unconfirmed_transaction(
         rng: &mut TestRng,
     ) -> (<CurrentNetwork as Network>::TransactionID, Data<Transaction<CurrentNetwork>>) {
-        // Sample a random fake transaction ID.
-        let id = Field::<CurrentNetwork>::rand(rng).into();
-        // Vary the size of the transactions.
-        let size = rng.gen_range(1024..10 * 1024);
-        // Sample random fake transaction bytes.
-        let mut vec = vec![0u8; size];
-        rng.fill_bytes(&mut vec);
-        let transaction = Data::Buffer(Bytes::from(vec));
-        // Return the ID and transaction.
-        (id, transaction)
+        let transaction = sample_execution_transaction_with_fee(false, rng);
+        let id = transaction.id();
+
+        (id, Data::Object(transaction))
     }
 
     // Creates a batch proposal with one solution and one transaction.
@@ -1862,25 +1983,30 @@ mod tests {
         round: u64,
         previous_certificate_ids: IndexSet<Field<CurrentNetwork>>,
         timestamp: i64,
+        num_transactions: u64,
         rng: &mut TestRng,
     ) -> Proposal<CurrentNetwork> {
+        let mut transmission_ids = IndexSet::new();
+        let mut transmissions = IndexMap::new();
+
+        // Prepare the solution and insert into the sets.
         let (solution_id, solution) = sample_unconfirmed_solution(rng);
-        let (transaction_id, transaction) = sample_unconfirmed_transaction(rng);
         let solution_checksum = solution.to_checksum::<CurrentNetwork>().unwrap();
-        let transaction_checksum = transaction.to_checksum::<CurrentNetwork>().unwrap();
-
         let solution_transmission_id = (solution_id, solution_checksum).into();
-        let transaction_transmission_id = (&transaction_id, &transaction_checksum).into();
+        transmission_ids.insert(solution_transmission_id);
+        transmissions.insert(solution_transmission_id, Transmission::Solution(solution));
+
+        // Prepare the transactions and insert into the sets.
+        for _ in 0..num_transactions {
+            let (transaction_id, transaction) = sample_unconfirmed_transaction(rng);
+            let transaction_checksum = transaction.to_checksum::<CurrentNetwork>().unwrap();
+            let transaction_transmission_id = (&transaction_id, &transaction_checksum).into();
+            transmission_ids.insert(transaction_transmission_id);
+            transmissions.insert(transaction_transmission_id, Transmission::Transaction(transaction));
+        }
 
         // Retrieve the private key.
         let private_key = author.private_key();
-        // Prepare the transmission IDs.
-        let transmission_ids = [solution_transmission_id, transaction_transmission_id].into();
-        let transmissions = [
-            (solution_transmission_id, Transmission::Solution(solution)),
-            (transaction_transmission_id, Transmission::Transaction(transaction)),
-        ]
-        .into();
         // Sign the batch header.
         let batch_header = BatchHeader::new(
             private_key,
@@ -2022,7 +2148,7 @@ mod tests {
     #[tokio::test]
     async fn test_propose_batch() {
         let mut rng = TestRng::default();
-        let (primary, _) = primary_without_handlers(&mut rng).await;
+        let (primary, _) = primary_without_handlers(&mut rng);
 
         // Check there is no batch currently proposed.
         assert!(primary.proposed_batch.read().is_none());
@@ -2043,7 +2169,7 @@ mod tests {
     #[tokio::test]
     async fn test_propose_batch_with_no_transmissions() {
         let mut rng = TestRng::default();
-        let (primary, _) = primary_without_handlers(&mut rng).await;
+        let (primary, _) = primary_without_handlers(&mut rng);
 
         // Check there is no batch currently proposed.
         assert!(primary.proposed_batch.read().is_none());
@@ -2057,7 +2183,7 @@ mod tests {
     async fn test_propose_batch_in_round() {
         let round = 3;
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
 
         // Fill primary storage.
         store_certificate_chain(&primary, &accounts, round, &mut rng);
@@ -2083,7 +2209,7 @@ mod tests {
         let round = 3;
         let prev_round = round - 1;
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
         let peer_account = &accounts[1];
         let peer_ip = peer_account.0;
 
@@ -2150,10 +2276,45 @@ mod tests {
         );
     }
 
+    #[tokio::test]
+    async fn test_propose_batch_over_spend_limit() {
+        let mut rng = TestRng::default();
+        // Create two primaries to test spend limit activation on V4.
+        let (accounts, committee) = sample_committee(&mut rng);
+        let primary = primary_with_committee(
+            0,
+            &accounts,
+            committee.clone(),
+            CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V3).unwrap(),
+        );
+
+        // Check there is no batch currently proposed.
+        assert!(primary.proposed_batch.read().is_none());
+        // Check the workers are empty.
+        primary.workers().iter().for_each(|worker| assert!(worker.transmissions().is_empty()));
+
+        // Generate a solution and a transaction.
+        let (solution_id, solution) = sample_unconfirmed_solution(&mut rng);
+        primary.workers[0].process_unconfirmed_solution(solution_id, solution).await.unwrap();
+
+        for _i in 0..5 {
+            let (transaction_id, transaction) = sample_unconfirmed_transaction(&mut rng);
+            // Store it on one of the workers.
+            primary.workers[0].process_unconfirmed_transaction(transaction_id, transaction).await.unwrap();
+        }
+
+        // Try to propose a batch again. This time, it should succeed.
+        assert!(primary.propose_batch().await.is_ok());
+        // Expect 2/5 transactions to be included in the proposal in addition to the solution.
+        assert_eq!(primary.proposed_batch.read().as_ref().unwrap().transmissions().len(), 3);
+        // Check the transmissions were correctly drained from the workers.
+        assert_eq!(primary.workers().iter().map(|worker| worker.transmissions().len()).sum::<usize>(), 3);
+    }
+
     #[tokio::test]
     async fn test_batch_propose_from_peer() {
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
 
         // Create a valid proposal with an author that isn't the primary.
         let round = 1;
@@ -2166,6 +2327,7 @@ mod tests {
             round,
             Default::default(),
             timestamp,
+            1,
             &mut rng,
         );
 
@@ -2188,7 +2350,7 @@ mod tests {
     #[tokio::test]
     async fn test_batch_propose_from_peer_when_not_synced() {
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
 
         // Create a valid proposal with an author that isn't the primary.
         let round = 1;
@@ -2201,6 +2363,7 @@ mod tests {
             round,
             Default::default(),
             timestamp,
+            1,
             &mut rng,
         );
 
@@ -2222,7 +2385,7 @@ mod tests {
     async fn test_batch_propose_from_peer_in_round() {
         let round = 2;
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
 
         // Generate certificates.
         let previous_certificates = store_certificate_chain(&primary, &accounts, round, &mut rng);
@@ -2237,6 +2400,7 @@ mod tests {
             round,
             previous_certificates,
             timestamp,
+            1,
             &mut rng,
         );
 
@@ -2257,7 +2421,7 @@ mod tests {
     #[tokio::test]
     async fn test_batch_propose_from_peer_wrong_round() {
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
 
         // Create a valid proposal with an author that isn't the primary.
         let round = 1;
@@ -2270,6 +2434,7 @@ mod tests {
             round,
             Default::default(),
             timestamp,
+            1,
             &mut rng,
         );
 
@@ -2299,7 +2464,7 @@ mod tests {
     async fn test_batch_propose_from_peer_in_round_wrong_round() {
         let round = 4;
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
 
         // Generate certificates.
         let previous_certificates = store_certificate_chain(&primary, &accounts, round, &mut rng);
@@ -2314,6 +2479,7 @@ mod tests {
             round,
             previous_certificates,
             timestamp,
+            1,
             &mut rng,
         );
 
@@ -2340,10 +2506,10 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn test_batch_propose_from_peer_with_invalid_timestamp() {
+    async fn test_batch_propose_from_peer_with_past_timestamp() {
         let round = 2;
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
 
         // Generate certificates.
         let previous_certificates = store_certificate_chain(&primary, &accounts, round, &mut rng);
@@ -2351,13 +2517,14 @@ mod tests {
         // Create a valid proposal with an author that isn't the primary.
         let peer_account = &accounts[1];
         let peer_ip = peer_account.0;
-        let invalid_timestamp = now(); // Use a timestamp that is too early.
+        let past_timestamp = now() - 100; // Use a timestamp that is in the past.
         let proposal = create_test_proposal(
             &peer_account.1,
             primary.ledger.current_committee().unwrap(),
             round,
             previous_certificates,
-            invalid_timestamp,
+            past_timestamp,
+            1,
             &mut rng,
         );
 
@@ -2378,40 +2545,57 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn test_batch_propose_from_peer_with_past_timestamp() {
-        let round = 2;
+    async fn test_batch_propose_from_peer_over_spend_limit() {
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
 
-        // Generate certificates.
-        let previous_certificates = store_certificate_chain(&primary, &accounts, round, &mut rng);
+        // Create two primaries to test spend limit activation on V4.
+        let (accounts, committee) = sample_committee(&mut rng);
+        let primary_v3 = primary_with_committee(
+            0,
+            &accounts,
+            committee.clone(),
+            CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V3).unwrap(),
+        );
+        let primary_v4 = primary_with_committee(
+            1,
+            &accounts,
+            committee.clone(),
+            CurrentNetwork::CONSENSUS_HEIGHT(ConsensusVersion::V4).unwrap(),
+        );
 
         // Create a valid proposal with an author that isn't the primary.
-        let peer_account = &accounts[1];
+        let round = 1;
+        let peer_account = &accounts[2];
         let peer_ip = peer_account.0;
-        let past_timestamp = now() - 5; // Use a timestamp that is in the past.
-        let proposal = create_test_proposal(
-            &peer_account.1,
-            primary.ledger.current_committee().unwrap(),
-            round,
-            previous_certificates,
-            past_timestamp,
-            &mut rng,
-        );
+        let timestamp = now() + MIN_BATCH_DELAY_IN_SECS as i64;
+        let proposal =
+            create_test_proposal(&peer_account.1, committee, round, Default::default(), timestamp, 4, &mut rng);
 
         // Make sure the primary is aware of the transmissions in the proposal.
         for (transmission_id, transmission) in proposal.transmissions() {
-            primary.workers[0].process_transmission_from_peer(peer_ip, *transmission_id, transmission.clone())
+            primary_v3.workers[0].process_transmission_from_peer(peer_ip, *transmission_id, transmission.clone());
+            primary_v4.workers[0].process_transmission_from_peer(peer_ip, *transmission_id, transmission.clone());
         }
 
         // The author must be known to resolver to pass propose checks.
-        primary.gateway.resolver().insert_peer(peer_ip, peer_ip, peer_account.1.address());
+        primary_v3.gateway.resolver().insert_peer(peer_ip, peer_ip, peer_account.1.address());
+        primary_v4.gateway.resolver().insert_peer(peer_ip, peer_ip, peer_account.1.address());
         // The primary must be considered synced.
-        primary.sync.block_sync().try_block_sync(&primary.gateway.clone()).await;
+        primary_v3.sync.block_sync().try_block_sync(&primary_v3.gateway.clone()).await;
+        primary_v4.sync.block_sync().try_block_sync(&primary_v4.gateway.clone()).await;
 
-        // Try to process the batch proposal from the peer, should error.
+        // Check the spend limit is enforced from V4 onwards.
         assert!(
-            primary.process_batch_propose_from_peer(peer_ip, (*proposal.batch_header()).clone().into()).await.is_err()
+            primary_v3
+                .process_batch_propose_from_peer(peer_ip, (*proposal.batch_header()).clone().into())
+                .await
+                .is_ok()
+        );
+        assert!(
+            primary_v4
+                .process_batch_propose_from_peer(peer_ip, (*proposal.batch_header()).clone().into())
+                .await
+                .is_err()
         );
     }
 
@@ -2419,7 +2603,7 @@ mod tests {
     async fn test_propose_batch_with_storage_round_behind_proposal_lock() {
         let round = 3;
         let mut rng = TestRng::default();
-        let (primary, _) = primary_without_handlers(&mut rng).await;
+        let (primary, _) = primary_without_handlers(&mut rng);
 
         // Check there is no batch currently proposed.
         assert!(primary.proposed_batch.read().is_none());
@@ -2452,7 +2636,7 @@ mod tests {
     async fn test_propose_batch_with_storage_round_behind_proposal() {
         let round = 5;
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
 
         // Generate previous certificates.
         let previous_certificates = store_certificate_chain(&primary, &accounts, round, &mut rng);
@@ -2465,6 +2649,7 @@ mod tests {
             round + 1,
             previous_certificates,
             timestamp,
+            1,
             &mut rng,
         );
 
@@ -2480,7 +2665,7 @@ mod tests {
     #[tokio::test(flavor = "multi_thread")]
     async fn test_batch_signature_from_peer() {
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
         map_account_addresses(&primary, &accounts);
 
         // Create a valid proposal.
@@ -2492,6 +2677,7 @@ mod tests {
             round,
             Default::default(),
             timestamp,
+            1,
             &mut rng,
         );
 
@@ -2516,7 +2702,7 @@ mod tests {
     async fn test_batch_signature_from_peer_in_round() {
         let round = 5;
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
         map_account_addresses(&primary, &accounts);
 
         // Generate certificates.
@@ -2530,6 +2716,7 @@ mod tests {
             round,
             previous_certificates,
             timestamp,
+            1,
             &mut rng,
         );
 
@@ -2553,7 +2740,7 @@ mod tests {
     #[tokio::test]
     async fn test_batch_signature_from_peer_no_quorum() {
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
         map_account_addresses(&primary, &accounts);
 
         // Create a valid proposal.
@@ -2565,6 +2752,7 @@ mod tests {
             round,
             Default::default(),
             timestamp,
+            1,
             &mut rng,
         );
 
@@ -2588,7 +2776,7 @@ mod tests {
     async fn test_batch_signature_from_peer_in_round_no_quorum() {
         let round = 7;
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
         map_account_addresses(&primary, &accounts);
 
         // Generate certificates.
@@ -2602,6 +2790,7 @@ mod tests {
             round,
             previous_certificates,
             timestamp,
+            1,
             &mut rng,
         );
 
@@ -2626,7 +2815,7 @@ mod tests {
         let round = 3;
         let prev_round = round - 1;
         let mut rng = TestRng::default();
-        let (primary, accounts) = primary_without_handlers(&mut rng).await;
+        let (primary, accounts) = primary_without_handlers(&mut rng);
         let peer_account = &accounts[1];
         let peer_ip = peer_account.0;
 
diff --git a/node/bft/src/worker.rs b/node/bft/src/worker.rs
index b55687b1e1..bc724c35e3 100644
--- a/node/bft/src/worker.rs
+++ b/node/bft/src/worker.rs
@@ -35,9 +35,9 @@ use snarkvm::{
 use colored::Colorize;
 use indexmap::{IndexMap, IndexSet};
 #[cfg(feature = "locktick")]
-use locktick::parking_lot::Mutex;
+use locktick::parking_lot::{Mutex, RwLock};
 #[cfg(not(feature = "locktick"))]
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RwLock};
 use rand::seq::IteratorRandom;
 use std::{future::Future, net::SocketAddr, sync::Arc, time::Duration};
 use tokio::{sync::oneshot, task::JoinHandle, time::timeout};
@@ -55,7 +55,7 @@ pub struct Worker<N: Network> {
     /// The proposed batch.
     proposed_batch: Arc<ProposedBatch<N>>,
     /// The ready queue.
-    ready: Ready<N>,
+    ready: Arc<RwLock<Ready<N>>>,
     /// The pending transmissions queue.
     pending: Arc<Pending<TransmissionID<N>, Transmission<N>>>,
     /// The spawned handles.
@@ -115,51 +115,51 @@ impl<N: Network> Worker<N> {
 
     /// Returns the number of transmissions in the ready queue.
     pub fn num_transmissions(&self) -> usize {
-        self.ready.num_transmissions()
+        self.ready.read().num_transmissions()
     }
 
     /// Returns the number of ratifications in the ready queue.
     pub fn num_ratifications(&self) -> usize {
-        self.ready.num_ratifications()
+        self.ready.read().num_ratifications()
     }
 
     /// Returns the number of solutions in the ready queue.
     pub fn num_solutions(&self) -> usize {
-        self.ready.num_solutions()
+        self.ready.read().num_solutions()
     }
 
     /// Returns the number of transactions in the ready queue.
     pub fn num_transactions(&self) -> usize {
-        self.ready.num_transactions()
+        self.ready.read().num_transactions()
     }
 }
 
 impl<N: Network> Worker<N> {
     /// Returns the transmission IDs in the ready queue.
     pub fn transmission_ids(&self) -> IndexSet<TransmissionID<N>> {
-        self.ready.transmission_ids()
+        self.ready.read().transmission_ids()
     }
 
     /// Returns the transmissions in the ready queue.
     pub fn transmissions(&self) -> IndexMap<TransmissionID<N>, Transmission<N>> {
-        self.ready.transmissions()
+        self.ready.read().transmissions()
     }
 
     /// Returns the solutions in the ready queue.
     pub fn solutions(&self) -> impl '_ + Iterator<Item = (SolutionID<N>, Data<Solution<N>>)> {
-        self.ready.solutions()
+        self.ready.read().solutions().into_iter()
     }
 
     /// Returns the transactions in the ready queue.
     pub fn transactions(&self) -> impl '_ + Iterator<Item = (N::TransactionID, Data<Transaction<N>>)> {
-        self.ready.transactions()
+        self.ready.read().transactions().into_iter()
     }
 }
 
 impl<N: Network> Worker<N> {
     /// Clears the solutions from the ready queue.
     pub(super) fn clear_solutions(&self) {
-        self.ready.clear_solutions()
+        self.ready.write().clear_solutions()
     }
 }
 
@@ -168,7 +168,7 @@ impl<N: Network> Worker<N> {
     pub fn contains_transmission(&self, transmission_id: impl Into<TransmissionID<N>>) -> bool {
         let transmission_id = transmission_id.into();
         // Check if the transmission ID exists in the ready queue, proposed batch, storage, or ledger.
-        self.ready.contains(transmission_id)
+        self.ready.read().contains(transmission_id)
             || self.proposed_batch.read().as_ref().map_or(false, |p| p.contains_transmission(transmission_id))
             || self.storage.contains_transmission(transmission_id)
             || self.ledger.contains_transmission(&transmission_id).unwrap_or(false)
@@ -180,7 +180,7 @@ impl<N: Network> Worker<N> {
     /// in the ledger are not guaranteed to be invalid for the current batch.
     pub fn get_transmission(&self, transmission_id: TransmissionID<N>) -> Option<Transmission<N>> {
         // Check if the transmission ID exists in the ready queue.
-        if let Some(transmission) = self.ready.get(transmission_id) {
+        if let Some(transmission) = self.ready.read().get(transmission_id) {
             return Some(transmission);
         }
         // Check if the transmission ID exists in storage.
@@ -214,9 +214,14 @@ impl<N: Network> Worker<N> {
         Ok((transmission_id, transmission))
     }
 
-    /// Removes up to the specified number of transmissions from the ready queue, and returns them.
-    pub(crate) fn drain(&self, num_transmissions: usize) -> impl Iterator<Item = (TransmissionID<N>, Transmission<N>)> {
-        self.ready.drain(num_transmissions).into_iter()
+    /// Inserts the transmission at the front of the ready queue.
+    pub(crate) fn insert_front(&self, key: TransmissionID<N>, value: Transmission<N>) {
+        self.ready.write().insert_front(key, value);
+    }
+
+    /// Removes and returns the transmission at the front of the ready queue.
+    pub(crate) fn remove_front(&self) -> Option<(TransmissionID<N>, Transmission<N>)> {
+        self.ready.write().remove_front()
     }
 
     /// Reinserts the specified transmission into the ready queue.
@@ -224,7 +229,7 @@ impl<N: Network> Worker<N> {
         // Check if the transmission ID exists.
         if !self.contains_transmission(transmission_id) {
             // Insert the transmission into the ready queue.
-            return self.ready.insert(transmission_id, transmission);
+            return self.ready.write().insert(transmission_id, transmission);
         }
         false
     }
@@ -234,6 +239,7 @@ impl<N: Network> Worker<N> {
         // Retrieve the transmission IDs.
         let transmission_ids = self
             .ready
+            .read()
             .transmission_ids()
             .into_iter()
             .choose_multiple(&mut rand::thread_rng(), Self::MAX_TRANSMISSIONS_PER_WORKER_PING)
@@ -256,7 +262,7 @@ impl<N: Network> Worker<N> {
         }
         // If the ready queue is full, then skip this transmission.
         // Note: We must prioritize the unconfirmed solutions and unconfirmed transactions, not transmissions.
-        if self.ready.num_transmissions() > Self::MAX_TRANSMISSIONS_PER_WORKER {
+        if self.ready.read().num_transmissions() > Self::MAX_TRANSMISSIONS_PER_WORKER {
             return;
         }
         // Attempt to fetch the transmission from the peer.
@@ -310,9 +316,10 @@ impl<N: Network> Worker<N> {
         };
         // If the transmission is a deserialized execution, verify it immediately.
         // This takes heavy transaction verification out of the hot path during block generation.
-        if let (TransmissionID::Transaction(tx_id, _), Transmission::Transaction(tx)) = (transmission_id, &transmission)
+        if let (TransmissionID::Transaction(tx_id, _), Transmission::Transaction(Data::Object(tx))) =
+            (transmission_id, &transmission)
         {
-            if let Data::Object(Transaction::Execute(..)) = tx {
+            if tx.is_execute() {
                 let self_ = self.clone();
                 let tx_ = tx.clone();
                 tokio::spawn(async move {
@@ -321,7 +328,7 @@ impl<N: Network> Worker<N> {
             }
         }
         // If the transmission ID and transmission type matches, then insert the transmission into the ready queue.
-        if is_well_formed && self.ready.insert(transmission_id, transmission) {
+        if is_well_formed && self.ready.write().insert(transmission_id, transmission) {
             trace!(
                 "Worker {} - Added transmission '{}.{}' from '{peer_ip}'",
                 self.id,
@@ -353,7 +360,7 @@ impl<N: Network> Worker<N> {
         // Check that the solution is well-formed and unique.
         self.ledger.check_solution_basic(solution_id, solution).await?;
         // Adds the solution to the ready queue.
-        if self.ready.insert(transmission_id, transmission) {
+        if self.ready.write().insert(transmission_id, transmission) {
             trace!(
                 "Worker {} - Added unconfirmed solution '{}.{}'",
                 self.id,
@@ -382,10 +389,18 @@ impl<N: Network> Worker<N> {
         if self.contains_transmission(transmission_id) {
             bail!("Transaction '{}.{}' already exists.", fmt_id(transaction_id), fmt_id(checksum).dimmed());
         }
+        // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
+        let transaction = spawn_blocking!({
+            match transaction {
+                Data::Object(transaction) => Ok(transaction),
+                Data::Buffer(bytes) => Ok(Transaction::<N>::read_le(&mut bytes.take(N::MAX_TRANSACTION_SIZE as u64))?),
+            }
+        })?;
+
         // Check that the transaction is well-formed and unique.
         self.ledger.check_transaction_basic(transaction_id, transaction).await?;
         // Adds the transaction to the ready queue.
-        if self.ready.insert(transmission_id, transmission) {
+        if self.ready.write().insert(transmission_id, transmission) {
             trace!(
                 "Worker {}.{} - Added unconfirmed transaction '{}'",
                 self.id,
@@ -547,6 +562,7 @@ mod tests {
         ledger::{
             block::Block,
             committee::Committee,
+            ledger_test_helpers::sample_execution_transaction_with_fee,
             narwhal::{BatchCertificate, Subdag, Transmission, TransmissionID},
         },
         prelude::Address,
@@ -608,7 +624,7 @@ mod tests {
             async fn check_transaction_basic(
                 &self,
                 transaction_id: N::TransactionID,
-                transaction: Data<Transaction<N>>,
+                transaction: Transaction<N>,
             ) -> Result<()>;
             fn check_next_block(&self, block: &Block<N>) -> Result<()>;
             fn prepare_advance_to_next_quorum_block(
@@ -617,6 +633,7 @@ mod tests {
                 transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
             ) -> Result<Block<N>>;
             fn advance_to_next_block(&self, block: &Block<N>) -> Result<()>;
+            fn transaction_spent_cost_in_microcredits(&self, transaction_id: N::TransactionID, transaction: Transaction<N>) -> Result<u64>;
         }
     }
 
@@ -671,12 +688,11 @@ mod tests {
         // Process the transmission.
         worker.process_transmission_from_peer(peer_ip, transmission_id, transmission.clone());
         assert!(worker.contains_transmission(transmission_id));
-        assert!(worker.ready.contains(transmission_id));
+        assert!(worker.ready.read().contains(transmission_id));
         assert_eq!(worker.get_transmission(transmission_id), Some(transmission));
         // Take the transmission from the ready set.
-        let transmission: Vec<_> = worker.drain(1).collect();
-        assert_eq!(transmission.len(), 1);
-        assert!(!worker.ready.contains(transmission_id));
+        assert!(worker.ready.write().remove_front().is_some());
+        assert!(!worker.ready.read().contains(transmission_id));
     }
 
     #[tokio::test]
@@ -753,7 +769,7 @@ mod tests {
         let result = worker.process_unconfirmed_solution(solution_id, solution).await;
         assert!(result.is_ok());
         assert!(!worker.pending.contains(transmission_id));
-        assert!(worker.ready.contains(transmission_id));
+        assert!(worker.ready.read().contains(transmission_id));
     }
 
     #[tokio::test]
@@ -790,12 +806,12 @@ mod tests {
         let result = worker.process_unconfirmed_solution(solution_id, solution).await;
         assert!(result.is_err());
         assert!(!worker.pending.contains(transmission_id));
-        assert!(!worker.ready.contains(transmission_id));
+        assert!(!worker.ready.read().contains(transmission_id));
     }
 
     #[tokio::test]
     async fn test_process_transaction_ok() {
-        let mut rng = &mut TestRng::default();
+        let rng = &mut TestRng::default();
         // Sample a committee.
         let committee = snarkvm::ledger::committee::test_helpers::sample_committee(rng);
         let committee_clone = committee.clone();
@@ -816,18 +832,19 @@ mod tests {
 
         // Create the Worker.
         let worker = Worker::new(0, Arc::new(gateway), storage, ledger, Default::default()).unwrap();
-        let transaction_id: <CurrentNetwork as Network>::TransactionID = Field::<CurrentNetwork>::rand(&mut rng).into();
-        let transaction = Data::Buffer(Bytes::from((0..512).map(|_| rng.gen::<u8>()).collect::<Vec<_>>()));
-        let checksum = transaction.to_checksum::<CurrentNetwork>().unwrap();
+        let transaction = sample_execution_transaction_with_fee(false, rng);
+        let transaction_id = transaction.id();
+        let transaction_data = Data::Object(transaction);
+        let checksum = transaction_data.to_checksum::<CurrentNetwork>().unwrap();
         let transmission_id = TransmissionID::Transaction(transaction_id, checksum);
         let worker_ = worker.clone();
         let peer_ip = SocketAddr::from(([127, 0, 0, 1], 1234));
         let _ = worker_.send_transmission_request(peer_ip, transmission_id).await;
         assert!(worker.pending.contains(transmission_id));
-        let result = worker.process_unconfirmed_transaction(transaction_id, transaction).await;
+        let result = worker.process_unconfirmed_transaction(transaction_id, transaction_data).await;
         assert!(result.is_ok());
         assert!(!worker.pending.contains(transmission_id));
-        assert!(worker.ready.contains(transmission_id));
+        assert!(worker.ready.read().contains(transmission_id));
     }
 
     #[tokio::test]
@@ -864,12 +881,12 @@ mod tests {
         let result = worker.process_unconfirmed_transaction(transaction_id, transaction).await;
         assert!(result.is_err());
         assert!(!worker.pending.contains(transmission_id));
-        assert!(!worker.ready.contains(transmission_id));
+        assert!(!worker.ready.read().contains(transmission_id));
     }
 
     #[tokio::test]
     async fn test_flood_transmission_requests() {
-        let mut rng = &mut TestRng::default();
+        let rng = &mut TestRng::default();
         // Sample a committee.
         let committee = snarkvm::ledger::committee::test_helpers::sample_committee(rng);
         let committee_clone = committee.clone();
@@ -890,9 +907,10 @@ mod tests {
 
         // Create the Worker.
         let worker = Worker::new(0, Arc::new(gateway), storage, ledger, Default::default()).unwrap();
-        let transaction_id: <CurrentNetwork as Network>::TransactionID = Field::<CurrentNetwork>::rand(&mut rng).into();
-        let transaction = Data::Buffer(Bytes::from((0..512).map(|_| rng.gen::<u8>()).collect::<Vec<_>>()));
-        let checksum = transaction.to_checksum::<CurrentNetwork>().unwrap();
+        let transaction = sample_execution_transaction_with_fee(false, rng);
+        let transaction_id = transaction.id();
+        let transaction_data = Data::Object(transaction);
+        let checksum = transaction_data.to_checksum::<CurrentNetwork>().unwrap();
         let transmission_id = TransmissionID::Transaction(transaction_id, checksum);
 
         // Determine the number of redundant requests are sent.
@@ -939,12 +957,12 @@ mod tests {
         assert_eq!(worker.pending.num_callbacks(transmission_id), num_flood_requests);
 
         // Check that fulfilling a transmission request clears the pending queue.
-        let result = worker.process_unconfirmed_transaction(transaction_id, transaction).await;
+        let result = worker.process_unconfirmed_transaction(transaction_id, transaction_data).await;
         assert!(result.is_ok());
         assert_eq!(worker.pending.num_sent_requests(transmission_id), 0);
         assert_eq!(worker.pending.num_callbacks(transmission_id), 0);
         assert!(!worker.pending.contains(transmission_id));
-        assert!(worker.ready.contains(transmission_id));
+        assert!(worker.ready.read().contains(transmission_id));
     }
 
     #[tokio::test]
diff --git a/node/rest/Cargo.toml b/node/rest/Cargo.toml
index e6d517d15c..d33dbc94af 100644
--- a/node/rest/Cargo.toml
+++ b/node/rest/Cargo.toml
@@ -80,7 +80,7 @@ version = "=3.3.2"
 [dependencies.snarkvm-synthesizer]
 # path = "../../../snarkVM/synthesizer"
 git = "https://github.com/ProvableHQ/snarkVM.git"
-rev = "10bcf43"
+rev = "7aaa46a"
 #version = "=1.3.0"
 default-features = false
 optional = true


Reviews:
user: login: vicsn
body: Hope it helps
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-01-22T16:17:32Z
user: login: niklaslong
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-01-23T09:22:12Z
user: login: niklaslong
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-01-23T09:28:37Z
user: login: niklaslong
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-01-23T09:44:40Z
user: login: vicsn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-01-23T11:07:23Z
user: login: vicsn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-01-23T11:12:43Z
user: login: niklaslong
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-01-23T14:22:00Z
user: login: vicsn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-01-23T14:50:32Z
user: login: raychu86
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-21T00:15:11Z
user: login: raychu86
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-21T00:23:06Z
user: login: raychu86
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-21T00:27:44Z
user: login: vicsn
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-21T08:36:00Z
user: login: niklaslong
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-23T09:24:37Z
user: login: raychu86
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T18:41:11Z
user: login: raychu86
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T18:43:02Z
user: login: niklaslong
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T13:39:28Z
user: login: acoglio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T17:10:38Z
user: login: raychu86
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T16:52:17Z
user: login: raychu86
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T16:52:40Z
user: login: niklaslong
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T17:14:55Z
user: login: acoglio
body: I'll just note here, for the record, the issue we already discussed. Two validators may happen to have different block heights when they check the same batch proposal, and thus have a slightly different idea of whether the batch proposal is valid or not. I don't think that this can cause forking, because a quorum of signatures, if achieved, is still a quorum. But it would be a cleaner story if the notion of whether a batch is valid at a given round only depended on the round number, which we could achieve by using round numbers instead of block heights to define different versions of batch proposal validity (such as cost limit).
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T23:24:23Z
user: login: vicsn
body: LGTM! Two nits. Might want to update the snarkVM rev one more time before merging.
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-24T08:07:00Z
user: login: niklaslong
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T12:35:19Z
user: login: niklaslong
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T16:14:51Z
user: login: vicsn
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-26T02:04:35Z

Review comments:
diff_hunk: @@ -404,4 +404,29 @@ impl<N: Network, C: ConsensusStorage<N>> LedgerService<N> for CoreLedgerService<
         tracing::info!("\n\nAdvanced to block {} at round {} - {}\n", block.height(), block.round(), block.hash());
         Ok(())
     }
+
+    fn compute_cost(&self, _transaction_id: N::TransactionID, transaction: Data<Transaction<N>>) -> Result<u64> {
+        // TODO: move to VM or ledger?
+        let process = self.ledger.vm().process();
+
+        // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
+        let transaction = match transaction {
path: node/bft/ledger-service/src/ledger.rs
user: login: vicsn
body: Deserialization is extremely expensive. I would consider moving this calculation into `ledger.rs:check_transaction_basic`, where we already deserialize. Perhaps that function can return the compute cost? Or some other design?
created_at: 2025-01-22T15:53:26Z
author_association: COLLABORATOR
diff_hunk: @@ -235,4 +235,9 @@ impl<N: Network> LedgerService<N> for MockLedgerService<N> {
         self.height_to_round_and_hash.lock().insert(block.height(), (block.round(), block.hash()));
         Ok(())
     }
+
+    /// TODO: is this reasonable?
path: node/bft/ledger-service/src/mock.rs
user: login: vicsn
body: > /// TODO: is this reasonable?

I guess if our mock ledger doesn't have the `compute_cost` function, yes its reasonable, no need to extend every test abstraction which exists.
created_at: 2025-01-22T15:54:28Z
author_association: COLLABORATOR
diff_hunk: @@ -186,4 +186,8 @@ impl<N: Network> LedgerService<N> for ProverLedgerService<N> {
     fn advance_to_next_block(&self, block: &Block<N>) -> Result<()> {
         bail!("Cannot advance to next block in prover - {block}")
     }
+
+    fn compute_cost(&self, transaction_id: N::TransactionID, _transaction: Data<Transaction<N>>) -> Result<u64> {
+        bail!("Transaction '{transaction_id}' doesn't exist in prover")
path: node/bft/ledger-service/src/prover.rs
user: login: vicsn
body: Not sure where this is used, will this break prover nodes?
created_at: 2025-01-22T15:55:37Z
author_association: COLLABORATOR
diff_hunk: @@ -490,88 +490,100 @@ impl<N: Network> Primary<N> {
 
         // Determined the required number of transmissions per worker.
         let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
+
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
+        // Keeps track of the number of transmissions included thus far. The
+        // transmissions index is only updated in batches, this counter is more granular.
+        let mut num_transmissions_included = 0usize;
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
         // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
+        'outer: for worker in self.workers.iter() {
             // Initialize a tracker for included transmissions for the current worker.
             let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
-                    break 'outer;
+            let mut worker_transmissions = worker.transmissions().into_iter();
path: node/bft/src/primary.rs
user: login: vicsn
body: So correct me if I'm wrong, but the previous behaviour was to **selectively drain** transmissions, and if they are duplicates or are incorrect, we discard them.

Your current implementation **completely drains** transmissions, so besides discaring duplicates or invalid ones, once we hit the size or compute limit, remaining transmissions are discarded.

Some options:
1. preprocess the compute spend of a transmission and to cache it in the worker's `Ready` queue. But this would unfortunately require a lot of code changes and wouldn't make sense everywhere.
2. We drain transmissions one by one. If we notice post-deserialization that we're above the compute limit, we reinsert that one transmission back into the queue using `worker.reinsert`. This has strictly less overhead than draining all transmissions.
created_at: 2025-01-22T16:13:01Z
author_association: COLLABORATOR
diff_hunk: @@ -490,88 +490,100 @@ impl<N: Network> Primary<N> {
 
         // Determined the required number of transmissions per worker.
         let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
+
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
+        // Keeps track of the number of transmissions included thus far. The
+        // transmissions index is only updated in batches, this counter is more granular.
+        let mut num_transmissions_included = 0usize;
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
         // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
+        'outer: for worker in self.workers.iter() {
             // Initialize a tracker for included transmissions for the current worker.
             let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
-                    break 'outer;
+            let mut worker_transmissions = worker.transmissions().into_iter();
+
+            // Check the transactions for inclusion in the batch proposal.
+            while num_transmissions_included_for_worker < num_transmissions_per_worker {
path: node/bft/src/primary.rs
user: login: vicsn
body: Wondering whether it is possible and desirable to put this check in the same place as the compute cost check, because they're both indicating "the batch proposal is too full now".

The loop could then be something like `While let Some(transmission) = worker.drain(1)`.

Up to you, just a suggestion
created_at: 2025-01-22T16:14:29Z
author_association: COLLABORATOR
diff_hunk: @@ -404,4 +404,29 @@ impl<N: Network, C: ConsensusStorage<N>> LedgerService<N> for CoreLedgerService<
         tracing::info!("\n\nAdvanced to block {} at round {} - {}\n", block.height(), block.round(), block.hash());
         Ok(())
     }
+
+    fn compute_cost(&self, _transaction_id: N::TransactionID, transaction: Data<Transaction<N>>) -> Result<u64> {
+        // TODO: move to VM or ledger?
+        let process = self.ledger.vm().process();
+
+        // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
+        let transaction = match transaction {
path: node/bft/ledger-service/src/ledger.rs
user: login: niklaslong
body: I did consider it and should revisit the idea but intuitively we might want to avoid coupling the cost calculation with `check_transaction_basic` as it's only called in `propose_batch` and not in `process_batch_propose_from_peer`, at least not directly. 
created_at: 2025-01-23T09:22:12Z
author_association: COLLABORATOR
diff_hunk: @@ -186,4 +186,8 @@ impl<N: Network> LedgerService<N> for ProverLedgerService<N> {
     fn advance_to_next_block(&self, block: &Block<N>) -> Result<()> {
         bail!("Cannot advance to next block in prover - {block}")
     }
+
+    fn compute_cost(&self, transaction_id: N::TransactionID, _transaction: Data<Transaction<N>>) -> Result<u64> {
+        bail!("Transaction '{transaction_id}' doesn't exist in prover")
path: node/bft/ledger-service/src/prover.rs
user: login: niklaslong
body: I don't think so, this function is called directly in the primary, which provers don't implement. Returning an error if a prover should attempt to call `compute_cost` is probably reasonable? 
created_at: 2025-01-23T09:28:37Z
author_association: COLLABORATOR
diff_hunk: @@ -490,88 +490,100 @@ impl<N: Network> Primary<N> {
 
         // Determined the required number of transmissions per worker.
         let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
+
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
+        // Keeps track of the number of transmissions included thus far. The
+        // transmissions index is only updated in batches, this counter is more granular.
+        let mut num_transmissions_included = 0usize;
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
         // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
+        'outer: for worker in self.workers.iter() {
             // Initialize a tracker for included transmissions for the current worker.
             let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
-                    break 'outer;
+            let mut worker_transmissions = worker.transmissions().into_iter();
path: node/bft/src/primary.rs
user: login: niklaslong
body: Previously the primary would preemptively drain the workers by batches, sized based on the remaining proposal space. This was fine as transactions were either included or dropped altogether but we need more nuance: we might choose to save a transaction for the following batch if it pushes the batch cost over the spend limit.

For the PoC, I opted to clone the transmissions (happens in `worker.transmissions()`), to avoid reinsertion logic initially, leaving the underlying collection untouched until the number of valid transactions is determined and draining it only at the end of the worker loop. Ideally, I'd want to `peek` the next item in the collection, and it might still be the most efficient. The next best thing would be to reinsert but that's still O(n) and requires more logic. 
created_at: 2025-01-23T09:44:39Z
author_association: COLLABORATOR
diff_hunk: @@ -404,4 +404,29 @@ impl<N: Network, C: ConsensusStorage<N>> LedgerService<N> for CoreLedgerService<
         tracing::info!("\n\nAdvanced to block {} at round {} - {}\n", block.height(), block.round(), block.hash());
         Ok(())
     }
+
+    fn compute_cost(&self, _transaction_id: N::TransactionID, transaction: Data<Transaction<N>>) -> Result<u64> {
+        // TODO: move to VM or ledger?
+        let process = self.ledger.vm().process();
+
+        // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
+        let transaction = match transaction {
path: node/bft/ledger-service/src/ledger.rs
user: login: vicsn
body: Ah good point... That is unfortunate, because in the case of `process_batch_propose_from_peer`, we might be retrieving the transmission from disk, in which case we'll most certainly have to incur the deserialization cost.

Maybe if we let `fn compute_cost` cost take in a `Transaction` instead of `Data<Transaction>`, it can at least be made explicit. For our own proposal we call it from within `check_transaction_basic`, for incoming proposals we'll need to deserialize before calling it.

And if it turns out to be a bottleneck, we can always refactor the locations where we deserialize more comprehensively, and potentially create a cache for the compute_cache if needed.
created_at: 2025-01-23T11:07:23Z
author_association: COLLABORATOR
diff_hunk: @@ -490,88 +490,100 @@ impl<N: Network> Primary<N> {
 
         // Determined the required number of transmissions per worker.
         let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
+
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
+        // Keeps track of the number of transmissions included thus far. The
+        // transmissions index is only updated in batches, this counter is more granular.
+        let mut num_transmissions_included = 0usize;
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
         // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
+        'outer: for worker in self.workers.iter() {
             // Initialize a tracker for included transmissions for the current worker.
             let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
-                    break 'outer;
+            let mut worker_transmissions = worker.transmissions().into_iter();
path: node/bft/src/primary.rs
user: login: vicsn
body: Peeking one by one would work!

Or draining one by one. If we optionally have to reinsert the very last one we drained if it crosses the compute threshold, I'm not sure what would be O(n)
created_at: 2025-01-23T11:12:43Z
author_association: COLLABORATOR
diff_hunk: @@ -490,88 +490,100 @@ impl<N: Network> Primary<N> {
 
         // Determined the required number of transmissions per worker.
         let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
+
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
+        // Keeps track of the number of transmissions included thus far. The
+        // transmissions index is only updated in batches, this counter is more granular.
+        let mut num_transmissions_included = 0usize;
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
         // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
+        'outer: for worker in self.workers.iter() {
             // Initialize a tracker for included transmissions for the current worker.
             let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
-                    break 'outer;
+            let mut worker_transmissions = worker.transmissions().into_iter();
path: node/bft/src/primary.rs
user: login: niklaslong
body: O(n) comes from shifting all the transmissions by one in the collection when reinserting at index 0 (to preserve fairness in the ordering of transmissions). Peeking directly likely won't work because it requires either holding the lock (not a good idea) or cloning. But perhaps doing the lookup one-by-one, followed by a removal could work. Though any operation that isn't inserting or removing values from the tail of the map will incur an O(n) shift cost. 
created_at: 2025-01-23T14:22:00Z
author_association: COLLABORATOR
diff_hunk: @@ -490,88 +490,100 @@ impl<N: Network> Primary<N> {
 
         // Determined the required number of transmissions per worker.
         let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
+
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
+        // Keeps track of the number of transmissions included thus far. The
+        // transmissions index is only updated in batches, this counter is more granular.
+        let mut num_transmissions_included = 0usize;
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
         // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
+        'outer: for worker in self.workers.iter() {
             // Initialize a tracker for included transmissions for the current worker.
             let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
-                    break 'outer;
+            let mut worker_transmissions = worker.transmissions().into_iter();
path: node/bft/src/primary.rs
user: login: vicsn
body: I see, thank you for explaining.

I take everything back, your approach seems close to optimal (assuming we don't change or introduce additional data structures).

> But perhaps doing the lookup one-by-one, followed by a removal could work. 

Indeed you can clone and lookup one-by-one (to avoid cloning the entire ready queue). And then batch drain at the end like you're already doing.
created_at: 2025-01-23T14:50:32Z
author_association: COLLABORATOR
diff_hunk: @@ -235,4 +235,10 @@ impl<N: Network> LedgerService<N> for MockLedgerService<N> {
         self.height_to_round_and_hash.lock().insert(block.height(), (block.round(), block.hash()));
         Ok(())
     }
+
+    /// Computes the execution cost in microcredits for a transaction.
+    fn compute_cost(&self, _transaction_id: N::TransactionID, _transaction: Data<Transaction<N>>) -> Result<u64> {
+        // Return 1 credit so this function can be used to test spend limits.
path: node/bft/ledger-service/src/mock.rs
user: login: raychu86
body: nit: 10_000_000 microcredits is 10 credits
created_at: 2025-02-21T00:15:11Z
author_association: COLLABORATOR
diff_hunk: @@ -488,90 +488,99 @@ impl<N: Network> Primary<N> {
             return Ok(());
         }
 
-        // Determined the required number of transmissions per worker.
-        let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
-        // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
-            // Initialize a tracker for included transmissions for the current worker.
-            let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
+        // Note: worker draining and transaction inclusion needs to be thought
+        // through carefully when there is more than one worker. The fairness
+        // provided by one worker (FIFO) is no longer guaranteed with multiple workers.
+        debug_assert_eq!(MAX_WORKERS, 1);
+
+        'outer: for worker in self.workers().iter() {
+            let mut num_worker_transmissions = 0usize;
+
+            // TODO(nkls): this is O(n), consider improving the underlying data structures.
+            while let Some((id, transmission)) = worker.shift_remove_front() {
+                if transmissions.len() == BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
                     break 'outer;
                 }
-                // Iterate through the worker transmissions.
-                'inner: for (id, transmission) in worker_transmissions {
-                    // Check if the ledger already contains the transmission.
-                    if self.ledger.contains_transmission(&id).unwrap_or(true) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
-                        continue 'inner;
-                    }
-                    // Check if the storage already contain the transmission.
-                    // Note: We do not skip if this is the first transmission in the proposal, to ensure that
-                    // the primary does not propose a batch with no transmissions.
-                    if !transmissions.is_empty() && self.storage.contains_transmission(id) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
-                        continue 'inner;
+
+                if num_worker_transmissions == Worker::<N>::MAX_TRANSMISSIONS_PER_WORKER {
+                    continue 'outer;
+                }
+
+                // Check if the ledger already contains the transmission.
+                if self.ledger.contains_transmission(&id).unwrap_or(true) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
+                    continue;
+                }
+
+                // Check if the storage already contain the transmission.
+                // Note: We do not skip if this is the first transmission in the proposal, to ensure that
+                // the primary does not propose a batch with no transmissions.
+                if !transmissions.is_empty() && self.storage.contains_transmission(id) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
+                    continue;
+                }
+
+                // Check the transmission is still valid.
+                match (id, transmission.clone()) {
+                    (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
+                        // Ensure the checksum matches. If not, skip the solution.
+                        if !matches!(solution.to_checksum::<N>(), Ok(solution_checksum) if solution_checksum == checksum)
+                        {
+                            trace!("Proposing - Skipping solution '{}' - Checksum mismatch", fmt_id(solution_id));
+                            continue;
+                        }
+                        // Check if the solution is still valid.
+                        if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
+                            trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
+                            continue;
+                        }
                     }
-                    // Check the transmission is still valid.
-                    match (id, transmission.clone()) {
-                        (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
-                            // Ensure the checksum matches.
-                            match solution.to_checksum::<N>() {
-                                Ok(solution_checksum) if solution_checksum == checksum => (),
-                                _ => {
-                                    trace!(
-                                        "Proposing - Skipping solution '{}' - Checksum mismatch",
-                                        fmt_id(solution_id)
-                                    );
-                                    continue 'inner;
-                                }
-                            }
-                            // Check if the solution is still valid.
-                            if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
-                                trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
-                                continue 'inner;
-                            }
+                    (TransmissionID::Transaction(transaction_id, checksum), Transmission::Transaction(transaction)) => {
+                        // Ensure the checksum matches. If not, skip the transaction.
+                        if !matches!(transaction.to_checksum::<N>(), Ok(transaction_checksum) if transaction_checksum == checksum )
+                        {
+                            trace!("Proposing - Skipping transaction '{}' - Checksum mismatch", fmt_id(transaction_id));
+                            continue;
                         }
-                        (
-                            TransmissionID::Transaction(transaction_id, checksum),
-                            Transmission::Transaction(transaction),
-                        ) => {
-                            // Ensure the checksum matches.
-                            match transaction.to_checksum::<N>() {
-                                Ok(transaction_checksum) if transaction_checksum == checksum => (),
+                        // Check if the transaction is still valid.
+                        // TODO: check if clone is cheap, otherwise fix.
+                        if let Err(e) = self.ledger.check_transaction_basic(transaction_id, transaction.clone()).await {
+                            trace!("Proposing - Skipping transaction '{}' - {e}", fmt_id(transaction_id));
+                            continue;
+                        }
+
+                        // Ensure the transaction doesn't bring the proposal above the spend limit.
+                        let block_height = self.ledger.latest_block_height() + 1;
+                        if N::CONSENSUS_VERSION(block_height)? >= ConsensusVersion::V4 {
+                            match self.ledger.compute_cost(transaction_id, transaction) {
+                                Ok(cost) if proposal_cost + cost <= N::BATCH_SPEND_LIMIT => proposal_cost += cost,
                                 _ => {
                                     trace!(
-                                        "Proposing - Skipping transaction '{}' - Checksum mismatch",
+                                        "Proposing - Skipping transaction '{}' - Batch spend limit surpassed",
                                         fmt_id(transaction_id)
                                     );
-                                    continue 'inner;
+
+                                    // Reinsert the transmission into the worker, O(n).
+                                    worker.shift_insert_front(id, transmission);
path: node/bft/src/primary.rs
user: login: raychu86
body: If there is a huge transmission that is 99% of the batch spend limit, isn't your worker never going to create a proposal with a larger utilization?

created_at: 2025-02-21T00:23:06Z
author_association: COLLABORATOR
diff_hunk: @@ -488,90 +488,99 @@ impl<N: Network> Primary<N> {
             return Ok(());
         }
 
-        // Determined the required number of transmissions per worker.
-        let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
-        // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
-            // Initialize a tracker for included transmissions for the current worker.
-            let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
+        // Note: worker draining and transaction inclusion needs to be thought
+        // through carefully when there is more than one worker. The fairness
+        // provided by one worker (FIFO) is no longer guaranteed with multiple workers.
+        debug_assert_eq!(MAX_WORKERS, 1);
+
+        'outer: for worker in self.workers().iter() {
+            let mut num_worker_transmissions = 0usize;
+
+            // TODO(nkls): this is O(n), consider improving the underlying data structures.
+            while let Some((id, transmission)) = worker.shift_remove_front() {
+                if transmissions.len() == BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
                     break 'outer;
                 }
-                // Iterate through the worker transmissions.
-                'inner: for (id, transmission) in worker_transmissions {
-                    // Check if the ledger already contains the transmission.
-                    if self.ledger.contains_transmission(&id).unwrap_or(true) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
-                        continue 'inner;
-                    }
-                    // Check if the storage already contain the transmission.
-                    // Note: We do not skip if this is the first transmission in the proposal, to ensure that
-                    // the primary does not propose a batch with no transmissions.
-                    if !transmissions.is_empty() && self.storage.contains_transmission(id) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
-                        continue 'inner;
+
+                if num_worker_transmissions == Worker::<N>::MAX_TRANSMISSIONS_PER_WORKER {
+                    continue 'outer;
+                }
+
+                // Check if the ledger already contains the transmission.
+                if self.ledger.contains_transmission(&id).unwrap_or(true) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
+                    continue;
+                }
+
+                // Check if the storage already contain the transmission.
+                // Note: We do not skip if this is the first transmission in the proposal, to ensure that
+                // the primary does not propose a batch with no transmissions.
+                if !transmissions.is_empty() && self.storage.contains_transmission(id) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
+                    continue;
+                }
+
+                // Check the transmission is still valid.
+                match (id, transmission.clone()) {
+                    (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
+                        // Ensure the checksum matches. If not, skip the solution.
+                        if !matches!(solution.to_checksum::<N>(), Ok(solution_checksum) if solution_checksum == checksum)
+                        {
+                            trace!("Proposing - Skipping solution '{}' - Checksum mismatch", fmt_id(solution_id));
+                            continue;
+                        }
+                        // Check if the solution is still valid.
+                        if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
+                            trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
+                            continue;
+                        }
                     }
-                    // Check the transmission is still valid.
-                    match (id, transmission.clone()) {
-                        (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
-                            // Ensure the checksum matches.
-                            match solution.to_checksum::<N>() {
-                                Ok(solution_checksum) if solution_checksum == checksum => (),
-                                _ => {
-                                    trace!(
-                                        "Proposing - Skipping solution '{}' - Checksum mismatch",
-                                        fmt_id(solution_id)
-                                    );
-                                    continue 'inner;
-                                }
-                            }
-                            // Check if the solution is still valid.
-                            if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
-                                trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
-                                continue 'inner;
-                            }
+                    (TransmissionID::Transaction(transaction_id, checksum), Transmission::Transaction(transaction)) => {
+                        // Ensure the checksum matches. If not, skip the transaction.
+                        if !matches!(transaction.to_checksum::<N>(), Ok(transaction_checksum) if transaction_checksum == checksum )
+                        {
+                            trace!("Proposing - Skipping transaction '{}' - Checksum mismatch", fmt_id(transaction_id));
+                            continue;
                         }
-                        (
-                            TransmissionID::Transaction(transaction_id, checksum),
-                            Transmission::Transaction(transaction),
-                        ) => {
-                            // Ensure the checksum matches.
-                            match transaction.to_checksum::<N>() {
-                                Ok(transaction_checksum) if transaction_checksum == checksum => (),
+                        // Check if the transaction is still valid.
+                        // TODO: check if clone is cheap, otherwise fix.
+                        if let Err(e) = self.ledger.check_transaction_basic(transaction_id, transaction.clone()).await {
+                            trace!("Proposing - Skipping transaction '{}' - {e}", fmt_id(transaction_id));
+                            continue;
+                        }
+
+                        // Ensure the transaction doesn't bring the proposal above the spend limit.
+                        let block_height = self.ledger.latest_block_height() + 1;
+                        if N::CONSENSUS_VERSION(block_height)? >= ConsensusVersion::V4 {
+                            match self.ledger.compute_cost(transaction_id, transaction) {
+                                Ok(cost) if proposal_cost + cost <= N::BATCH_SPEND_LIMIT => proposal_cost += cost,
                                 _ => {
                                     trace!(
-                                        "Proposing - Skipping transaction '{}' - Checksum mismatch",
+                                        "Proposing - Skipping transaction '{}' - Batch spend limit surpassed",
                                         fmt_id(transaction_id)
                                     );
-                                    continue 'inner;
+
+                                    // Reinsert the transmission into the worker, O(n).
+                                    worker.shift_insert_front(id, transmission);
path: node/bft/src/primary.rs
user: login: raychu86
body: Might be smarter to pull other transactions that might fill the remaining space of the proposal, with smaller transactions. 

This also might be an opportune moment to finally implement the use of `priority_fee`, which was previously not factored into the ordering
created_at: 2025-02-21T00:27:44Z
author_association: COLLABORATOR
diff_hunk: @@ -488,90 +488,99 @@ impl<N: Network> Primary<N> {
             return Ok(());
         }
 
-        // Determined the required number of transmissions per worker.
-        let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
-        // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
-            // Initialize a tracker for included transmissions for the current worker.
-            let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
+        // Note: worker draining and transaction inclusion needs to be thought
+        // through carefully when there is more than one worker. The fairness
+        // provided by one worker (FIFO) is no longer guaranteed with multiple workers.
+        debug_assert_eq!(MAX_WORKERS, 1);
+
+        'outer: for worker in self.workers().iter() {
+            let mut num_worker_transmissions = 0usize;
+
+            // TODO(nkls): this is O(n), consider improving the underlying data structures.
+            while let Some((id, transmission)) = worker.shift_remove_front() {
+                if transmissions.len() == BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
                     break 'outer;
                 }
-                // Iterate through the worker transmissions.
-                'inner: for (id, transmission) in worker_transmissions {
-                    // Check if the ledger already contains the transmission.
-                    if self.ledger.contains_transmission(&id).unwrap_or(true) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
-                        continue 'inner;
-                    }
-                    // Check if the storage already contain the transmission.
-                    // Note: We do not skip if this is the first transmission in the proposal, to ensure that
-                    // the primary does not propose a batch with no transmissions.
-                    if !transmissions.is_empty() && self.storage.contains_transmission(id) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
-                        continue 'inner;
+
+                if num_worker_transmissions == Worker::<N>::MAX_TRANSMISSIONS_PER_WORKER {
+                    continue 'outer;
+                }
+
+                // Check if the ledger already contains the transmission.
+                if self.ledger.contains_transmission(&id).unwrap_or(true) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
+                    continue;
+                }
+
+                // Check if the storage already contain the transmission.
+                // Note: We do not skip if this is the first transmission in the proposal, to ensure that
+                // the primary does not propose a batch with no transmissions.
+                if !transmissions.is_empty() && self.storage.contains_transmission(id) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
+                    continue;
+                }
+
+                // Check the transmission is still valid.
+                match (id, transmission.clone()) {
+                    (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
+                        // Ensure the checksum matches. If not, skip the solution.
+                        if !matches!(solution.to_checksum::<N>(), Ok(solution_checksum) if solution_checksum == checksum)
+                        {
+                            trace!("Proposing - Skipping solution '{}' - Checksum mismatch", fmt_id(solution_id));
+                            continue;
+                        }
+                        // Check if the solution is still valid.
+                        if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
+                            trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
+                            continue;
+                        }
                     }
-                    // Check the transmission is still valid.
-                    match (id, transmission.clone()) {
-                        (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
-                            // Ensure the checksum matches.
-                            match solution.to_checksum::<N>() {
-                                Ok(solution_checksum) if solution_checksum == checksum => (),
-                                _ => {
-                                    trace!(
-                                        "Proposing - Skipping solution '{}' - Checksum mismatch",
-                                        fmt_id(solution_id)
-                                    );
-                                    continue 'inner;
-                                }
-                            }
-                            // Check if the solution is still valid.
-                            if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
-                                trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
-                                continue 'inner;
-                            }
+                    (TransmissionID::Transaction(transaction_id, checksum), Transmission::Transaction(transaction)) => {
+                        // Ensure the checksum matches. If not, skip the transaction.
+                        if !matches!(transaction.to_checksum::<N>(), Ok(transaction_checksum) if transaction_checksum == checksum )
+                        {
+                            trace!("Proposing - Skipping transaction '{}' - Checksum mismatch", fmt_id(transaction_id));
+                            continue;
                         }
-                        (
-                            TransmissionID::Transaction(transaction_id, checksum),
-                            Transmission::Transaction(transaction),
-                        ) => {
-                            // Ensure the checksum matches.
-                            match transaction.to_checksum::<N>() {
-                                Ok(transaction_checksum) if transaction_checksum == checksum => (),
+                        // Check if the transaction is still valid.
+                        // TODO: check if clone is cheap, otherwise fix.
+                        if let Err(e) = self.ledger.check_transaction_basic(transaction_id, transaction.clone()).await {
+                            trace!("Proposing - Skipping transaction '{}' - {e}", fmt_id(transaction_id));
+                            continue;
+                        }
+
+                        // Ensure the transaction doesn't bring the proposal above the spend limit.
+                        let block_height = self.ledger.latest_block_height() + 1;
+                        if N::CONSENSUS_VERSION(block_height)? >= ConsensusVersion::V4 {
+                            match self.ledger.compute_cost(transaction_id, transaction) {
+                                Ok(cost) if proposal_cost + cost <= N::BATCH_SPEND_LIMIT => proposal_cost += cost,
                                 _ => {
                                     trace!(
-                                        "Proposing - Skipping transaction '{}' - Checksum mismatch",
+                                        "Proposing - Skipping transaction '{}' - Batch spend limit surpassed",
                                         fmt_id(transaction_id)
                                     );
-                                    continue 'inner;
+
+                                    // Reinsert the transmission into the worker, O(n).
+                                    worker.shift_insert_front(id, transmission);
path: node/bft/src/primary.rs
user: login: vicsn
body: I think the priority fee should be tackled independently, it'll be a decent change to review:
- need to insert ahead of the transactions_queue (draft PR exists)
- need to insert ahead of the ready queue is a good extra idea.
created_at: 2025-02-21T08:36:00Z
author_association: COLLABORATOR
diff_hunk: @@ -488,90 +488,99 @@ impl<N: Network> Primary<N> {
             return Ok(());
         }
 
-        // Determined the required number of transmissions per worker.
-        let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
-        // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
-            // Initialize a tracker for included transmissions for the current worker.
-            let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
+        // Note: worker draining and transaction inclusion needs to be thought
+        // through carefully when there is more than one worker. The fairness
+        // provided by one worker (FIFO) is no longer guaranteed with multiple workers.
+        debug_assert_eq!(MAX_WORKERS, 1);
+
+        'outer: for worker in self.workers().iter() {
+            let mut num_worker_transmissions = 0usize;
+
+            // TODO(nkls): this is O(n), consider improving the underlying data structures.
+            while let Some((id, transmission)) = worker.shift_remove_front() {
+                if transmissions.len() == BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
                     break 'outer;
                 }
-                // Iterate through the worker transmissions.
-                'inner: for (id, transmission) in worker_transmissions {
-                    // Check if the ledger already contains the transmission.
-                    if self.ledger.contains_transmission(&id).unwrap_or(true) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
-                        continue 'inner;
-                    }
-                    // Check if the storage already contain the transmission.
-                    // Note: We do not skip if this is the first transmission in the proposal, to ensure that
-                    // the primary does not propose a batch with no transmissions.
-                    if !transmissions.is_empty() && self.storage.contains_transmission(id) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
-                        continue 'inner;
+
+                if num_worker_transmissions == Worker::<N>::MAX_TRANSMISSIONS_PER_WORKER {
+                    continue 'outer;
+                }
+
+                // Check if the ledger already contains the transmission.
+                if self.ledger.contains_transmission(&id).unwrap_or(true) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
+                    continue;
+                }
+
+                // Check if the storage already contain the transmission.
+                // Note: We do not skip if this is the first transmission in the proposal, to ensure that
+                // the primary does not propose a batch with no transmissions.
+                if !transmissions.is_empty() && self.storage.contains_transmission(id) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
+                    continue;
+                }
+
+                // Check the transmission is still valid.
+                match (id, transmission.clone()) {
+                    (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
+                        // Ensure the checksum matches. If not, skip the solution.
+                        if !matches!(solution.to_checksum::<N>(), Ok(solution_checksum) if solution_checksum == checksum)
+                        {
+                            trace!("Proposing - Skipping solution '{}' - Checksum mismatch", fmt_id(solution_id));
+                            continue;
+                        }
+                        // Check if the solution is still valid.
+                        if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
+                            trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
+                            continue;
+                        }
                     }
-                    // Check the transmission is still valid.
-                    match (id, transmission.clone()) {
-                        (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
-                            // Ensure the checksum matches.
-                            match solution.to_checksum::<N>() {
-                                Ok(solution_checksum) if solution_checksum == checksum => (),
-                                _ => {
-                                    trace!(
-                                        "Proposing - Skipping solution '{}' - Checksum mismatch",
-                                        fmt_id(solution_id)
-                                    );
-                                    continue 'inner;
-                                }
-                            }
-                            // Check if the solution is still valid.
-                            if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
-                                trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
-                                continue 'inner;
-                            }
+                    (TransmissionID::Transaction(transaction_id, checksum), Transmission::Transaction(transaction)) => {
+                        // Ensure the checksum matches. If not, skip the transaction.
+                        if !matches!(transaction.to_checksum::<N>(), Ok(transaction_checksum) if transaction_checksum == checksum )
+                        {
+                            trace!("Proposing - Skipping transaction '{}' - Checksum mismatch", fmt_id(transaction_id));
+                            continue;
                         }
-                        (
-                            TransmissionID::Transaction(transaction_id, checksum),
-                            Transmission::Transaction(transaction),
-                        ) => {
-                            // Ensure the checksum matches.
-                            match transaction.to_checksum::<N>() {
-                                Ok(transaction_checksum) if transaction_checksum == checksum => (),
+                        // Check if the transaction is still valid.
+                        // TODO: check if clone is cheap, otherwise fix.
+                        if let Err(e) = self.ledger.check_transaction_basic(transaction_id, transaction.clone()).await {
+                            trace!("Proposing - Skipping transaction '{}' - {e}", fmt_id(transaction_id));
+                            continue;
+                        }
+
+                        // Ensure the transaction doesn't bring the proposal above the spend limit.
+                        let block_height = self.ledger.latest_block_height() + 1;
+                        if N::CONSENSUS_VERSION(block_height)? >= ConsensusVersion::V4 {
+                            match self.ledger.compute_cost(transaction_id, transaction) {
+                                Ok(cost) if proposal_cost + cost <= N::BATCH_SPEND_LIMIT => proposal_cost += cost,
                                 _ => {
                                     trace!(
-                                        "Proposing - Skipping transaction '{}' - Checksum mismatch",
+                                        "Proposing - Skipping transaction '{}' - Batch spend limit surpassed",
                                         fmt_id(transaction_id)
                                     );
-                                    continue 'inner;
+
+                                    // Reinsert the transmission into the worker, O(n).
+                                    worker.shift_insert_front(id, transmission);
path: node/bft/src/primary.rs
user: login: niklaslong
body: > If there is a huge transmission that is 99% of the batch spend limit, isn't your worker never going to create a proposal with a larger utilization?

Indeed, we don't currently attempt to fill the batch with smaller transmissions after the huge one is skipped. We could maintain _some_ degree of fairness by continuing to fill batch `n` until the spend limit or the batch size is reached and prioritise the large skipped transmission(s) in batch `n + 1`. 

Not quite sure if new vectors are introduced with this approach or if it should be bundled with the priority fee changes but happy to make the adjustment here as well. 
created_at: 2025-02-23T09:24:37Z
author_association: COLLABORATOR
diff_hunk: @@ -768,6 +787,47 @@ impl<N: Network> Primary<N> {
         // Inserts the missing transmissions into the workers.
         self.insert_missing_transmissions_into_workers(peer_ip, missing_transmissions.into_iter())?;
 
+        // Ensure the transaction doesn't bring the proposal above the spend limit.
+        let block_height = self.ledger.latest_block_height() + 1;
+        if N::CONSENSUS_VERSION(block_height)? >= ConsensusVersion::V4 {
path: node/bft/src/primary.rs
user: login: raychu86
body: Note that the Consensus/BFT modules do not care about block heights and only care about rounds. 

This means different validators may be at same round, but different block height if one of the validators have not received enough certificates to advance their ledger. This means they may process incoming proposals differently, but neither are technically wrong from their point of view.  
created_at: 2025-02-28T18:41:10Z
author_association: COLLABORATOR
diff_hunk: @@ -768,6 +787,47 @@ impl<N: Network> Primary<N> {
         // Inserts the missing transmissions into the workers.
         self.insert_missing_transmissions_into_workers(peer_ip, missing_transmissions.into_iter())?;
 
+        // Ensure the transaction doesn't bring the proposal above the spend limit.
+        let block_height = self.ledger.latest_block_height() + 1;
+        if N::CONSENSUS_VERSION(block_height)? >= ConsensusVersion::V4 {
path: node/bft/src/primary.rs
user: login: raychu86
body: Intuitively, this likely won't be an issue if the validator from behind syncs up with other certificates and catches their ledger up before processing the incoming proposal.
created_at: 2025-02-28T18:43:02Z
author_association: COLLABORATOR
diff_hunk: @@ -768,6 +787,47 @@ impl<N: Network> Primary<N> {
         // Inserts the missing transmissions into the workers.
         self.insert_missing_transmissions_into_workers(peer_ip, missing_transmissions.into_iter())?;
 
+        // Ensure the transaction doesn't bring the proposal above the spend limit.
+        let block_height = self.ledger.latest_block_height() + 1;
+        if N::CONSENSUS_VERSION(block_height)? >= ConsensusVersion::V4 {
path: node/bft/src/primary.rs
user: login: niklaslong
body: Let me check my understanding: the edge case is a straggling node processing transmissions into a batch without limiting spend when the limit is already enforced by other validators? Eventually all nodes should process the same subdag into a block at the same height so unless there is a quorum of nodes behind (impossible by definition?) there's no chance of a spend limit going unenforced because of asynchrony during the consensus upgrade? 

In other words, despite rounds not matching, block height is still dependable as a trigger for new logic in these modules? 🤔 
created_at: 2025-03-03T13:39:28Z
author_association: COLLABORATOR
diff_hunk: @@ -768,6 +787,47 @@ impl<N: Network> Primary<N> {
         // Inserts the missing transmissions into the workers.
         self.insert_missing_transmissions_into_workers(peer_ip, missing_transmissions.into_iter())?;
 
+        // Ensure the transaction doesn't bring the proposal above the spend limit.
+        let block_height = self.ledger.latest_block_height() + 1;
+        if N::CONSENSUS_VERSION(block_height)? >= ConsensusVersion::V4 {
path: node/bft/src/primary.rs
user: login: acoglio
body: Perhaps we could use a lookback approach for how (the heights of) blocks determine the consensus version active at each round, similarly to how blocks determine the active (= lookback) committee at each round.
created_at: 2025-03-03T17:10:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -488,90 +488,114 @@ impl<N: Network> Primary<N> {
             return Ok(());
         }
 
-        // Determined the required number of transmissions per worker.
-        let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
-        // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
-            // Initialize a tracker for included transmissions for the current worker.
-            let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
+        // Note: worker draining and transaction inclusion needs to be thought
+        // through carefully when there is more than one worker. The fairness
+        // provided by one worker (FIFO) is no longer guaranteed with multiple workers.
+        debug_assert_eq!(MAX_WORKERS, 1);
+
+        'outer: for worker in self.workers().iter() {
+            let mut num_worker_transmissions = 0usize;
+
+            while let Some((id, transmission)) = worker.remove_front() {
+                if transmissions.len() == BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
                     break 'outer;
                 }
-                // Iterate through the worker transmissions.
-                'inner: for (id, transmission) in worker_transmissions {
-                    // Check if the ledger already contains the transmission.
-                    if self.ledger.contains_transmission(&id).unwrap_or(true) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
-                        continue 'inner;
-                    }
-                    // Check if the storage already contain the transmission.
-                    // Note: We do not skip if this is the first transmission in the proposal, to ensure that
-                    // the primary does not propose a batch with no transmissions.
-                    if !transmissions.is_empty() && self.storage.contains_transmission(id) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
-                        continue 'inner;
+
+                if num_worker_transmissions == Worker::<N>::MAX_TRANSMISSIONS_PER_WORKER {
+                    continue 'outer;
+                }
+
+                // Check if the ledger already contains the transmission.
+                if self.ledger.contains_transmission(&id).unwrap_or(true) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
+                    continue;
+                }
+
+                // Check if the storage already contain the transmission.
+                // Note: We do not skip if this is the first transmission in the proposal, to ensure that
+                // the primary does not propose a batch with no transmissions.
+                if !transmissions.is_empty() && self.storage.contains_transmission(id) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
+                    continue;
+                }
+
+                // Check the transmission is still valid.
+                match (id, transmission.clone()) {
+                    (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
+                        // Ensure the checksum matches. If not, skip the solution.
+                        if !matches!(solution.to_checksum::<N>(), Ok(solution_checksum) if solution_checksum == checksum)
+                        {
+                            trace!("Proposing - Skipping solution '{}' - Checksum mismatch", fmt_id(solution_id));
+                            continue;
+                        }
+                        // Check if the solution is still valid.
+                        if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
+                            trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
+                            continue;
+                        }
                     }
-                    // Check the transmission is still valid.
-                    match (id, transmission.clone()) {
-                        (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
-                            // Ensure the checksum matches.
-                            match solution.to_checksum::<N>() {
-                                Ok(solution_checksum) if solution_checksum == checksum => (),
-                                _ => {
-                                    trace!(
-                                        "Proposing - Skipping solution '{}' - Checksum mismatch",
-                                        fmt_id(solution_id)
-                                    );
-                                    continue 'inner;
+                    (TransmissionID::Transaction(transaction_id, checksum), Transmission::Transaction(transaction)) => {
+                        // Ensure the checksum matches. If not, skip the transaction.
+                        if !matches!(transaction.to_checksum::<N>(), Ok(transaction_checksum) if transaction_checksum == checksum )
+                        {
+                            trace!("Proposing - Skipping transaction '{}' - Checksum mismatch", fmt_id(transaction_id));
+                            continue;
+                        }
+
+                        // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
+                        let transaction = spawn_blocking!({
+                            match transaction {
+                                Data::Object(transaction) => Ok(transaction),
+                                Data::Buffer(bytes) => {
+                                    Ok(Transaction::<N>::read_le(&mut bytes.take(N::MAX_TRANSACTION_SIZE as u64))?)
                                 }
                             }
-                            // Check if the solution is still valid.
-                            if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
-                                trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
-                                continue 'inner;
-                            }
+                        })?;
+
+                        // Check if the transaction is still valid.
+                        // TODO: check if clone is cheap, otherwise fix.
+                        if let Err(e) = self.ledger.check_transaction_basic(transaction_id, transaction.clone()).await {
+                            trace!("Proposing - Skipping transaction '{}' - {e}", fmt_id(transaction_id));
+                            continue;
                         }
-                        (
-                            TransmissionID::Transaction(transaction_id, checksum),
-                            Transmission::Transaction(transaction),
-                        ) => {
-                            // Ensure the checksum matches.
-                            match transaction.to_checksum::<N>() {
-                                Ok(transaction_checksum) if transaction_checksum == checksum => (),
+
+                        // Ensure the transaction doesn't bring the proposal above the spend limit.
+                        //
+                        // Validators start following the new consensus rules early, in order to guarantee a smooth transition.
+                        // The activation height is offset by the maximum number of blocks that can be produced within gc to
+                        // ensure honest validators can't mismatch due to differing views of the dag.
+                        let block_height =
+                            self.ledger.latest_block_height() + 1 + (BatchHeader::<N>::MAX_GC_ROUNDS as u32 / 2);
path: node/bft/src/primary.rs
user: login: raychu86
body: nit: use `saturating_*` for adds and divs
created_at: 2025-03-12T16:52:16Z
author_association: COLLABORATOR
diff_hunk: @@ -488,90 +488,114 @@ impl<N: Network> Primary<N> {
             return Ok(());
         }
 
-        // Determined the required number of transmissions per worker.
-        let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
-        // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
-            // Initialize a tracker for included transmissions for the current worker.
-            let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
+        // Note: worker draining and transaction inclusion needs to be thought
+        // through carefully when there is more than one worker. The fairness
+        // provided by one worker (FIFO) is no longer guaranteed with multiple workers.
+        debug_assert_eq!(MAX_WORKERS, 1);
+
+        'outer: for worker in self.workers().iter() {
+            let mut num_worker_transmissions = 0usize;
+
+            while let Some((id, transmission)) = worker.remove_front() {
+                if transmissions.len() == BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
                     break 'outer;
                 }
-                // Iterate through the worker transmissions.
-                'inner: for (id, transmission) in worker_transmissions {
-                    // Check if the ledger already contains the transmission.
-                    if self.ledger.contains_transmission(&id).unwrap_or(true) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
-                        continue 'inner;
-                    }
-                    // Check if the storage already contain the transmission.
-                    // Note: We do not skip if this is the first transmission in the proposal, to ensure that
-                    // the primary does not propose a batch with no transmissions.
-                    if !transmissions.is_empty() && self.storage.contains_transmission(id) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
-                        continue 'inner;
+
+                if num_worker_transmissions == Worker::<N>::MAX_TRANSMISSIONS_PER_WORKER {
+                    continue 'outer;
+                }
+
+                // Check if the ledger already contains the transmission.
+                if self.ledger.contains_transmission(&id).unwrap_or(true) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
+                    continue;
+                }
+
+                // Check if the storage already contain the transmission.
+                // Note: We do not skip if this is the first transmission in the proposal, to ensure that
+                // the primary does not propose a batch with no transmissions.
+                if !transmissions.is_empty() && self.storage.contains_transmission(id) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
+                    continue;
+                }
+
+                // Check the transmission is still valid.
+                match (id, transmission.clone()) {
+                    (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
+                        // Ensure the checksum matches. If not, skip the solution.
+                        if !matches!(solution.to_checksum::<N>(), Ok(solution_checksum) if solution_checksum == checksum)
+                        {
+                            trace!("Proposing - Skipping solution '{}' - Checksum mismatch", fmt_id(solution_id));
+                            continue;
+                        }
+                        // Check if the solution is still valid.
+                        if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
+                            trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
+                            continue;
+                        }
                     }
-                    // Check the transmission is still valid.
-                    match (id, transmission.clone()) {
-                        (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
-                            // Ensure the checksum matches.
-                            match solution.to_checksum::<N>() {
-                                Ok(solution_checksum) if solution_checksum == checksum => (),
-                                _ => {
-                                    trace!(
-                                        "Proposing - Skipping solution '{}' - Checksum mismatch",
-                                        fmt_id(solution_id)
-                                    );
-                                    continue 'inner;
+                    (TransmissionID::Transaction(transaction_id, checksum), Transmission::Transaction(transaction)) => {
+                        // Ensure the checksum matches. If not, skip the transaction.
+                        if !matches!(transaction.to_checksum::<N>(), Ok(transaction_checksum) if transaction_checksum == checksum )
+                        {
+                            trace!("Proposing - Skipping transaction '{}' - Checksum mismatch", fmt_id(transaction_id));
+                            continue;
+                        }
+
+                        // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
+                        let transaction = spawn_blocking!({
+                            match transaction {
+                                Data::Object(transaction) => Ok(transaction),
+                                Data::Buffer(bytes) => {
+                                    Ok(Transaction::<N>::read_le(&mut bytes.take(N::MAX_TRANSACTION_SIZE as u64))?)
                                 }
                             }
-                            // Check if the solution is still valid.
-                            if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
-                                trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
-                                continue 'inner;
-                            }
+                        })?;
+
+                        // Check if the transaction is still valid.
+                        // TODO: check if clone is cheap, otherwise fix.
+                        if let Err(e) = self.ledger.check_transaction_basic(transaction_id, transaction.clone()).await {
+                            trace!("Proposing - Skipping transaction '{}' - {e}", fmt_id(transaction_id));
+                            continue;
                         }
-                        (
-                            TransmissionID::Transaction(transaction_id, checksum),
-                            Transmission::Transaction(transaction),
-                        ) => {
-                            // Ensure the checksum matches.
-                            match transaction.to_checksum::<N>() {
-                                Ok(transaction_checksum) if transaction_checksum == checksum => (),
+
+                        // Ensure the transaction doesn't bring the proposal above the spend limit.
+                        //
+                        // Validators start following the new consensus rules early, in order to guarantee a smooth transition.
+                        // The activation height is offset by the maximum number of blocks that can be produced within gc to
+                        // ensure honest validators can't mismatch due to differing views of the dag.
+                        let block_height =
+                            self.ledger.latest_block_height() + 1 + (BatchHeader::<N>::MAX_GC_ROUNDS as u32 / 2);
path: node/bft/src/primary.rs
user: login: raychu86
body: Also maybe add further explanation on why the specific range was selected. 
created_at: 2025-03-12T16:52:40Z
author_association: COLLABORATOR
diff_hunk: @@ -488,90 +488,114 @@ impl<N: Network> Primary<N> {
             return Ok(());
         }
 
-        // Determined the required number of transmissions per worker.
-        let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
-        // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
-            // Initialize a tracker for included transmissions for the current worker.
-            let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
+        // Note: worker draining and transaction inclusion needs to be thought
+        // through carefully when there is more than one worker. The fairness
+        // provided by one worker (FIFO) is no longer guaranteed with multiple workers.
+        debug_assert_eq!(MAX_WORKERS, 1);
+
+        'outer: for worker in self.workers().iter() {
+            let mut num_worker_transmissions = 0usize;
+
+            while let Some((id, transmission)) = worker.remove_front() {
+                if transmissions.len() == BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
                     break 'outer;
                 }
-                // Iterate through the worker transmissions.
-                'inner: for (id, transmission) in worker_transmissions {
-                    // Check if the ledger already contains the transmission.
-                    if self.ledger.contains_transmission(&id).unwrap_or(true) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
-                        continue 'inner;
-                    }
-                    // Check if the storage already contain the transmission.
-                    // Note: We do not skip if this is the first transmission in the proposal, to ensure that
-                    // the primary does not propose a batch with no transmissions.
-                    if !transmissions.is_empty() && self.storage.contains_transmission(id) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
-                        continue 'inner;
+
+                if num_worker_transmissions == Worker::<N>::MAX_TRANSMISSIONS_PER_WORKER {
+                    continue 'outer;
+                }
+
+                // Check if the ledger already contains the transmission.
+                if self.ledger.contains_transmission(&id).unwrap_or(true) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
+                    continue;
+                }
+
+                // Check if the storage already contain the transmission.
+                // Note: We do not skip if this is the first transmission in the proposal, to ensure that
+                // the primary does not propose a batch with no transmissions.
+                if !transmissions.is_empty() && self.storage.contains_transmission(id) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
+                    continue;
+                }
+
+                // Check the transmission is still valid.
+                match (id, transmission.clone()) {
+                    (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
+                        // Ensure the checksum matches. If not, skip the solution.
+                        if !matches!(solution.to_checksum::<N>(), Ok(solution_checksum) if solution_checksum == checksum)
+                        {
+                            trace!("Proposing - Skipping solution '{}' - Checksum mismatch", fmt_id(solution_id));
+                            continue;
+                        }
+                        // Check if the solution is still valid.
+                        if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
+                            trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
+                            continue;
+                        }
                     }
-                    // Check the transmission is still valid.
-                    match (id, transmission.clone()) {
-                        (TransmissionID::Solution(solution_id, checksum), Transmission::Solution(solution)) => {
-                            // Ensure the checksum matches.
-                            match solution.to_checksum::<N>() {
-                                Ok(solution_checksum) if solution_checksum == checksum => (),
-                                _ => {
-                                    trace!(
-                                        "Proposing - Skipping solution '{}' - Checksum mismatch",
-                                        fmt_id(solution_id)
-                                    );
-                                    continue 'inner;
+                    (TransmissionID::Transaction(transaction_id, checksum), Transmission::Transaction(transaction)) => {
+                        // Ensure the checksum matches. If not, skip the transaction.
+                        if !matches!(transaction.to_checksum::<N>(), Ok(transaction_checksum) if transaction_checksum == checksum )
+                        {
+                            trace!("Proposing - Skipping transaction '{}' - Checksum mismatch", fmt_id(transaction_id));
+                            continue;
+                        }
+
+                        // Deserialize the transaction. If the transaction exceeds the maximum size, then return an error.
+                        let transaction = spawn_blocking!({
+                            match transaction {
+                                Data::Object(transaction) => Ok(transaction),
+                                Data::Buffer(bytes) => {
+                                    Ok(Transaction::<N>::read_le(&mut bytes.take(N::MAX_TRANSACTION_SIZE as u64))?)
                                 }
                             }
-                            // Check if the solution is still valid.
-                            if let Err(e) = self.ledger.check_solution_basic(solution_id, solution).await {
-                                trace!("Proposing - Skipping solution '{}' - {e}", fmt_id(solution_id));
-                                continue 'inner;
-                            }
+                        })?;
+
+                        // Check if the transaction is still valid.
+                        // TODO: check if clone is cheap, otherwise fix.
+                        if let Err(e) = self.ledger.check_transaction_basic(transaction_id, transaction.clone()).await {
+                            trace!("Proposing - Skipping transaction '{}' - {e}", fmt_id(transaction_id));
+                            continue;
                         }
-                        (
-                            TransmissionID::Transaction(transaction_id, checksum),
-                            Transmission::Transaction(transaction),
-                        ) => {
-                            // Ensure the checksum matches.
-                            match transaction.to_checksum::<N>() {
-                                Ok(transaction_checksum) if transaction_checksum == checksum => (),
+
+                        // Ensure the transaction doesn't bring the proposal above the spend limit.
+                        //
+                        // Validators start following the new consensus rules early, in order to guarantee a smooth transition.
+                        // The activation height is offset by the maximum number of blocks that can be produced within gc to
+                        // ensure honest validators can't mismatch due to differing views of the dag.
+                        let block_height =
+                            self.ledger.latest_block_height() + 1 + (BatchHeader::<N>::MAX_GC_ROUNDS as u32 / 2);
path: node/bft/src/primary.rs
user: login: niklaslong
body: Added in [17ddf90](https://github.com/ProvableHQ/snarkOS/pull/3471/commits/17ddf908b06dae6aac0f812a6df7cc8338fecc07). 
created_at: 2025-03-13T17:14:55Z
author_association: COLLABORATOR
diff_hunk: @@ -45,105 +47,152 @@ impl<N: Network> Default for Ready<N> {
 impl<N: Network> Ready<N> {
     /// Initializes a new instance of the ready queue.
     pub fn new() -> Self {
-        Self { transmissions: Default::default() }
+        Self { transmission_ids: Default::default(), transmissions: Default::default(), offset: Default::default() }
     }
 
     /// Returns `true` if the ready queue is empty.
     pub fn is_empty(&self) -> bool {
-        self.transmissions.read().is_empty()
+        self.transmissions.is_empty()
     }
 
     /// Returns the number of transmissions in the ready queue.
     pub fn num_transmissions(&self) -> usize {
-        self.transmissions.read().len()
+        self.transmissions.len()
     }
 
     /// Returns the number of ratifications in the ready queue.
     pub fn num_ratifications(&self) -> usize {
-        self.transmissions.read().keys().filter(|id| matches!(id, TransmissionID::Ratification)).count()
+        self.transmission_ids.keys().filter(|id| matches!(id, TransmissionID::Ratification)).count()
     }
 
     /// Returns the number of solutions in the ready queue.
     pub fn num_solutions(&self) -> usize {
-        self.transmissions.read().keys().filter(|id| matches!(id, TransmissionID::Solution(..))).count()
+        self.transmission_ids.keys().filter(|id| matches!(id, TransmissionID::Solution(..))).count()
     }
 
     /// Returns the number of transactions in the ready queue.
     pub fn num_transactions(&self) -> usize {
-        self.transmissions.read().keys().filter(|id| matches!(id, TransmissionID::Transaction(..))).count()
+        self.transmission_ids.keys().filter(|id| matches!(id, TransmissionID::Transaction(..))).count()
     }
 
     /// Returns the transmission IDs in the ready queue.
     pub fn transmission_ids(&self) -> IndexSet<TransmissionID<N>> {
-        self.transmissions.read().keys().copied().collect()
+        self.transmission_ids.keys().copied().collect()
     }
 
     /// Returns the transmissions in the ready queue.
     pub fn transmissions(&self) -> IndexMap<TransmissionID<N>, Transmission<N>> {
-        self.transmissions.read().clone()
+        self.transmissions.iter().cloned().collect()
     }
 
     /// Returns the solutions in the ready queue.
-    pub fn solutions(&self) -> impl '_ + Iterator<Item = (SolutionID<N>, Data<Solution<N>>)> {
-        self.transmissions.read().clone().into_iter().filter_map(|(id, transmission)| match (id, transmission) {
-            (TransmissionID::Solution(id, _), Transmission::Solution(solution)) => Some((id, solution)),
-            _ => None,
-        })
+    pub fn solutions(&self) -> Vec<(SolutionID<N>, Data<Solution<N>>)> {
+        self.transmissions
+            .iter()
+            .filter_map(|(id, transmission)| match (id, transmission) {
+                (TransmissionID::Solution(id, _), Transmission::Solution(solution)) => Some((*id, solution.clone())),
+                _ => None,
+            })
+            .collect()
     }
 
     /// Returns the transactions in the ready queue.
-    pub fn transactions(&self) -> impl '_ + Iterator<Item = (N::TransactionID, Data<Transaction<N>>)> {
-        self.transmissions.read().clone().into_iter().filter_map(|(id, transmission)| match (id, transmission) {
-            (TransmissionID::Transaction(id, _), Transmission::Transaction(tx)) => Some((id, tx)),
-            _ => None,
-        })
+    pub fn transactions(&self) -> Vec<(N::TransactionID, Data<Transaction<N>>)> {
+        self.transmissions
+            .iter()
+            .filter_map(|(id, transmission)| match (id, transmission) {
+                (TransmissionID::Transaction(id, _), Transmission::Transaction(tx)) => Some((*id, tx.clone())),
+                _ => None,
+            })
+            .collect()
     }
 }
 
 impl<N: Network> Ready<N> {
     /// Returns `true` if the ready queue contains the specified `transmission ID`.
     pub fn contains(&self, transmission_id: impl Into<TransmissionID<N>>) -> bool {
-        self.transmissions.read().contains_key(&transmission_id.into())
+        self.transmission_ids.contains_key(&transmission_id.into())
     }
 
     /// Returns the transmission, given the specified `transmission ID`.
     pub fn get(&self, transmission_id: impl Into<TransmissionID<N>>) -> Option<Transmission<N>> {
-        self.transmissions.read().get(&transmission_id.into()).cloned()
+        self.transmission_ids
+            .get(&transmission_id.into())
+            .and_then(|&index| self.transmissions.get((index - self.offset) as usize))
+            .map(|(_, transmission)| transmission.clone())
     }
 
     /// Inserts the specified (`transmission ID`, `transmission`) to the ready queue.
     /// Returns `true` if the transmission is new, and was added to the ready queue.
-    pub fn insert(&self, transmission_id: impl Into<TransmissionID<N>>, transmission: Transmission<N>) -> bool {
+    pub fn insert(&mut self, transmission_id: impl Into<TransmissionID<N>>, transmission: Transmission<N>) -> bool {
+        let physical_index = self.transmissions.len();
         let transmission_id = transmission_id.into();
-        // Insert the transmission ID.
-        let is_new = self.transmissions.write().insert(transmission_id, transmission).is_none();
-        // Return whether the transmission is new.
-        is_new
+
+        if let Vacant(entry) = self.transmission_ids.entry(transmission_id) {
+            entry.insert(physical_index as i64 + self.offset);
+            self.transmissions.push_back((transmission_id, transmission));
+            true
+        } else {
+            false
+        }
+    }
+
+    /// Inserts the specified (`transmission ID`, `transmission`) at the front
+    /// of the ready queue.
+    /// Returns `true` if the transmission is new, and was added to the ready queue.
+    pub fn insert_front(
+        &mut self,
+        transmission_id: impl Into<TransmissionID<N>>,
+        transmission: Transmission<N>,
+    ) -> bool {
+        let transmission_id = transmission_id.into();
+        if let Vacant(entry) = self.transmission_ids.entry(transmission_id) {
+            self.offset -= 1;
+            let index = self.offset;
+
+            entry.insert(index);
+            self.transmissions.push_front((transmission_id, transmission));
+            true
+        } else {
+            false
+        }
     }
 
-    /// Removes up to the specified number of transmissions and returns them.
-    pub fn drain(&self, num_transmissions: usize) -> IndexMap<TransmissionID<N>, Transmission<N>> {
-        // Acquire the write lock.
-        let mut transmissions = self.transmissions.write();
-        // Determine the number of transmissions to drain.
-        let range = 0..transmissions.len().min(num_transmissions);
-        // Drain the transmission IDs.
-        transmissions.drain(range).collect::<IndexMap<_, _>>()
+    /// Removes and returns the transmission at the front of the queue.
+    pub fn remove_front(&mut self) -> Option<(TransmissionID<N>, Transmission<N>)> {
+        if let Some((transmission_id, transmission)) = self.transmissions.pop_front() {
+            self.transmission_ids.remove(&transmission_id);
+
+            if self.transmission_ids.is_empty() {
+                debug_assert!(self.transmissions.is_empty());
+                self.offset = 0;
path: node/bft/src/helpers/ready.rs
user: login: vicsn
body: Could we theoretically also have a debug_assert that it should equal 0?
created_at: 2025-03-24T07:42:04Z
author_association: COLLABORATOR
diff_hunk: @@ -493,90 +493,131 @@ impl<N: Network> Primary<N> {
             return Ok(());
         }
 
-        // Determined the required number of transmissions per worker.
-        let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
-        // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
-            // Initialize a tracker for included transmissions for the current worker.
-            let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
+        // Note: worker draining and transaction inclusion needs to be thought
+        // through carefully when there is more than one worker. The fairness
+        // provided by one worker (FIFO) is no longer guaranteed with multiple workers.
+        debug_assert_eq!(MAX_WORKERS, 1);
+
+        'outer: for worker in self.workers().iter() {
+            let mut num_worker_transmissions = 0usize;
+
+            while let Some((id, transmission)) = worker.remove_front() {
+                // Check the selected transmissions are below the batch limit.
+                if transmissions.len() >= BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
                     break 'outer;
                 }
-                // Iterate through the worker transmissions.
-                'inner: for (id, transmission) in worker_transmissions {
-                    // Check if the ledger already contains the transmission.
-                    if self.ledger.contains_transmission(&id).unwrap_or(true) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
-                        continue 'inner;
-                    }
-                    // Check if the storage already contain the transmission.
-                    // Note: We do not skip if this is the first transmission in the proposal, to ensure that
-                    // the primary does not propose a batch with no transmissions.
-                    if !transmissions.is_empty() && self.storage.contains_transmission(id) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
-                        continue 'inner;
+
+                // Check the max transmissions per worker is not exceeded.
+                if num_worker_transmissions >= Worker::<N>::MAX_TRANSMISSIONS_PER_WORKER {
+                    continue 'outer;
+                }
+
+                // Check if the ledger already contains the transmission.
+                if self.ledger.contains_transmission(&id).unwrap_or(true) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
+                    continue;
+                }
+
+                // Check if the storage already contain the transmission.
+                // Note: We do not skip if this is the first transmission in the proposal, to ensure that
path: node/bft/src/primary.rs
user: login: vicsn
body: Want to make an issue to get rid of this in the future? Empty proposals are allowed, and I assume if its in storage it'll likely be added to ledger.
created_at: 2025-03-24T07:52:39Z
author_association: COLLABORATOR
diff_hunk: @@ -45,105 +47,152 @@ impl<N: Network> Default for Ready<N> {
 impl<N: Network> Ready<N> {
     /// Initializes a new instance of the ready queue.
     pub fn new() -> Self {
-        Self { transmissions: Default::default() }
+        Self { transmission_ids: Default::default(), transmissions: Default::default(), offset: Default::default() }
     }
 
     /// Returns `true` if the ready queue is empty.
     pub fn is_empty(&self) -> bool {
-        self.transmissions.read().is_empty()
+        self.transmissions.is_empty()
     }
 
     /// Returns the number of transmissions in the ready queue.
     pub fn num_transmissions(&self) -> usize {
-        self.transmissions.read().len()
+        self.transmissions.len()
     }
 
     /// Returns the number of ratifications in the ready queue.
     pub fn num_ratifications(&self) -> usize {
-        self.transmissions.read().keys().filter(|id| matches!(id, TransmissionID::Ratification)).count()
+        self.transmission_ids.keys().filter(|id| matches!(id, TransmissionID::Ratification)).count()
     }
 
     /// Returns the number of solutions in the ready queue.
     pub fn num_solutions(&self) -> usize {
-        self.transmissions.read().keys().filter(|id| matches!(id, TransmissionID::Solution(..))).count()
+        self.transmission_ids.keys().filter(|id| matches!(id, TransmissionID::Solution(..))).count()
     }
 
     /// Returns the number of transactions in the ready queue.
     pub fn num_transactions(&self) -> usize {
-        self.transmissions.read().keys().filter(|id| matches!(id, TransmissionID::Transaction(..))).count()
+        self.transmission_ids.keys().filter(|id| matches!(id, TransmissionID::Transaction(..))).count()
     }
 
     /// Returns the transmission IDs in the ready queue.
     pub fn transmission_ids(&self) -> IndexSet<TransmissionID<N>> {
-        self.transmissions.read().keys().copied().collect()
+        self.transmission_ids.keys().copied().collect()
     }
 
     /// Returns the transmissions in the ready queue.
     pub fn transmissions(&self) -> IndexMap<TransmissionID<N>, Transmission<N>> {
-        self.transmissions.read().clone()
+        self.transmissions.iter().cloned().collect()
     }
 
     /// Returns the solutions in the ready queue.
-    pub fn solutions(&self) -> impl '_ + Iterator<Item = (SolutionID<N>, Data<Solution<N>>)> {
-        self.transmissions.read().clone().into_iter().filter_map(|(id, transmission)| match (id, transmission) {
-            (TransmissionID::Solution(id, _), Transmission::Solution(solution)) => Some((id, solution)),
-            _ => None,
-        })
+    pub fn solutions(&self) -> Vec<(SolutionID<N>, Data<Solution<N>>)> {
+        self.transmissions
+            .iter()
+            .filter_map(|(id, transmission)| match (id, transmission) {
+                (TransmissionID::Solution(id, _), Transmission::Solution(solution)) => Some((*id, solution.clone())),
+                _ => None,
+            })
+            .collect()
     }
 
     /// Returns the transactions in the ready queue.
-    pub fn transactions(&self) -> impl '_ + Iterator<Item = (N::TransactionID, Data<Transaction<N>>)> {
-        self.transmissions.read().clone().into_iter().filter_map(|(id, transmission)| match (id, transmission) {
-            (TransmissionID::Transaction(id, _), Transmission::Transaction(tx)) => Some((id, tx)),
-            _ => None,
-        })
+    pub fn transactions(&self) -> Vec<(N::TransactionID, Data<Transaction<N>>)> {
+        self.transmissions
+            .iter()
+            .filter_map(|(id, transmission)| match (id, transmission) {
+                (TransmissionID::Transaction(id, _), Transmission::Transaction(tx)) => Some((*id, tx.clone())),
+                _ => None,
+            })
+            .collect()
     }
 }
 
 impl<N: Network> Ready<N> {
     /// Returns `true` if the ready queue contains the specified `transmission ID`.
     pub fn contains(&self, transmission_id: impl Into<TransmissionID<N>>) -> bool {
-        self.transmissions.read().contains_key(&transmission_id.into())
+        self.transmission_ids.contains_key(&transmission_id.into())
     }
 
     /// Returns the transmission, given the specified `transmission ID`.
     pub fn get(&self, transmission_id: impl Into<TransmissionID<N>>) -> Option<Transmission<N>> {
-        self.transmissions.read().get(&transmission_id.into()).cloned()
+        self.transmission_ids
+            .get(&transmission_id.into())
+            .and_then(|&index| self.transmissions.get((index - self.offset) as usize))
+            .map(|(_, transmission)| transmission.clone())
     }
 
     /// Inserts the specified (`transmission ID`, `transmission`) to the ready queue.
     /// Returns `true` if the transmission is new, and was added to the ready queue.
-    pub fn insert(&self, transmission_id: impl Into<TransmissionID<N>>, transmission: Transmission<N>) -> bool {
+    pub fn insert(&mut self, transmission_id: impl Into<TransmissionID<N>>, transmission: Transmission<N>) -> bool {
+        let physical_index = self.transmissions.len();
         let transmission_id = transmission_id.into();
-        // Insert the transmission ID.
-        let is_new = self.transmissions.write().insert(transmission_id, transmission).is_none();
-        // Return whether the transmission is new.
-        is_new
+
+        if let Vacant(entry) = self.transmission_ids.entry(transmission_id) {
+            entry.insert(physical_index as i64 + self.offset);
+            self.transmissions.push_back((transmission_id, transmission));
+            true
+        } else {
+            false
+        }
+    }
+
+    /// Inserts the specified (`transmission ID`, `transmission`) at the front
+    /// of the ready queue.
+    /// Returns `true` if the transmission is new, and was added to the ready queue.
+    pub fn insert_front(
+        &mut self,
+        transmission_id: impl Into<TransmissionID<N>>,
+        transmission: Transmission<N>,
+    ) -> bool {
+        let transmission_id = transmission_id.into();
+        if let Vacant(entry) = self.transmission_ids.entry(transmission_id) {
+            self.offset -= 1;
+            let index = self.offset;
+
+            entry.insert(index);
+            self.transmissions.push_front((transmission_id, transmission));
+            true
+        } else {
+            false
+        }
     }
 
-    /// Removes up to the specified number of transmissions and returns them.
-    pub fn drain(&self, num_transmissions: usize) -> IndexMap<TransmissionID<N>, Transmission<N>> {
-        // Acquire the write lock.
-        let mut transmissions = self.transmissions.write();
-        // Determine the number of transmissions to drain.
-        let range = 0..transmissions.len().min(num_transmissions);
-        // Drain the transmission IDs.
-        transmissions.drain(range).collect::<IndexMap<_, _>>()
+    /// Removes and returns the transmission at the front of the queue.
+    pub fn remove_front(&mut self) -> Option<(TransmissionID<N>, Transmission<N>)> {
+        if let Some((transmission_id, transmission)) = self.transmissions.pop_front() {
+            self.transmission_ids.remove(&transmission_id);
+
+            if self.transmission_ids.is_empty() {
+                debug_assert!(self.transmissions.is_empty());
+                self.offset = 0;
path: node/bft/src/helpers/ready.rs
user: login: niklaslong
body: Almost (I thought this might be the case as we don't currently have a method for removing from the back) but consider the following: an equal number of inserts (no change to the offset when adding to the back of the queue) followed by removals at the front (increments offset) would result in a positive offset when the queue is empty again. 
created_at: 2025-03-24T12:35:19Z
author_association: COLLABORATOR
diff_hunk: @@ -493,90 +493,131 @@ impl<N: Network> Primary<N> {
             return Ok(());
         }
 
-        // Determined the required number of transmissions per worker.
-        let num_transmissions_per_worker = BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH / self.num_workers() as usize;
         // Initialize the map of transmissions.
         let mut transmissions: IndexMap<_, _> = Default::default();
-        // Take the transmissions from the workers.
-        for worker in self.workers.iter() {
-            // Initialize a tracker for included transmissions for the current worker.
-            let mut num_transmissions_included_for_worker = 0;
-            // Keep draining the worker until the desired number of transmissions is reached or the worker is empty.
-            'outer: while num_transmissions_included_for_worker < num_transmissions_per_worker {
-                // Determine the number of remaining transmissions for the worker.
-                let num_remaining_transmissions =
-                    num_transmissions_per_worker.saturating_sub(num_transmissions_included_for_worker);
-                // Drain the worker.
-                let mut worker_transmissions = worker.drain(num_remaining_transmissions).peekable();
-                // If the worker is empty, break early.
-                if worker_transmissions.peek().is_none() {
+        // Track the total execution costs of the batch proposal as it is being constructed.
+        let mut proposal_cost = 0u64;
+        // Note: worker draining and transaction inclusion needs to be thought
+        // through carefully when there is more than one worker. The fairness
+        // provided by one worker (FIFO) is no longer guaranteed with multiple workers.
+        debug_assert_eq!(MAX_WORKERS, 1);
+
+        'outer: for worker in self.workers().iter() {
+            let mut num_worker_transmissions = 0usize;
+
+            while let Some((id, transmission)) = worker.remove_front() {
+                // Check the selected transmissions are below the batch limit.
+                if transmissions.len() >= BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH {
                     break 'outer;
                 }
-                // Iterate through the worker transmissions.
-                'inner: for (id, transmission) in worker_transmissions {
-                    // Check if the ledger already contains the transmission.
-                    if self.ledger.contains_transmission(&id).unwrap_or(true) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
-                        continue 'inner;
-                    }
-                    // Check if the storage already contain the transmission.
-                    // Note: We do not skip if this is the first transmission in the proposal, to ensure that
-                    // the primary does not propose a batch with no transmissions.
-                    if !transmissions.is_empty() && self.storage.contains_transmission(id) {
-                        trace!("Proposing - Skipping transmission '{}' - Already in storage", fmt_id(id));
-                        continue 'inner;
+
+                // Check the max transmissions per worker is not exceeded.
+                if num_worker_transmissions >= Worker::<N>::MAX_TRANSMISSIONS_PER_WORKER {
+                    continue 'outer;
+                }
+
+                // Check if the ledger already contains the transmission.
+                if self.ledger.contains_transmission(&id).unwrap_or(true) {
+                    trace!("Proposing - Skipping transmission '{}' - Already in ledger", fmt_id(id));
+                    continue;
+                }
+
+                // Check if the storage already contain the transmission.
+                // Note: We do not skip if this is the first transmission in the proposal, to ensure that
path: node/bft/src/primary.rs
user: login: niklaslong
body: I've opened https://github.com/ProvableHQ/snarkOS/issues/3551. 
created_at: 2025-03-25T16:14:51Z
author_association: COLLABORATOR

PR comments:
user: login: niklaslong
created_at: 2025-02-23T09:30:08Z
body: Note to reviewers: I have modified the ready queue to support O(1) operations at the front, giving us an efficient fifo. This makes batch building logic simpler because we can optimistically remove the transmission from the queue before processing it for inclusion, with reinsertions at the front being required **only** for valid transmissions that bring the batch above the spend limit. This may also be useful for implementing priority fees in future. 
author_association: COLLABORATOR
user: login: niklaslong
created_at: 2025-03-04T15:37:23Z
body: Rebased
author_association: COLLABORATOR
user: login: zklimaleo
created_at: 2025-03-16T03:16:40Z
body: I'm actually curious whether if this new limit will reject large block like this from mainnet outage recover? And does that means it can never recover if so? https://aleoscan.io/block?h=5213428 @vicsn 
  
 
author_association: NONE
user: login: vicsn
created_at: 2025-03-16T07:32:41Z
body: > I'm actually curious whether if this new limit will reject large block like this from mainnet outage recover? And does that means it can never recover if so? https://aleoscan.io/block?h=5213428 @vicsn

Thx for asking! The new limit applies to **proposals** and will limit the maximum compute used in a block. It is likely that the block you refer to still falls within the margin. If it doesn't, it is not an issue, as the limit only becomes effective from a certain future block height onwards.
author_association: COLLABORATOR
user: login: zklimaleo
created_at: 2025-03-16T08:03:37Z
body: > Thx for asking! The new limit applies to **proposals** and will limit the maximum compute used in a block. It is likely that the block you refer to still falls within the margin. If it doesn't, it is not an issue, as the limit only becomes effective from a certain future block height onwards.

@vicsn Thanks! It's more of a concern for future outage, maybe it's helpful to also make sure the validators are able to spread out transactions to different blocks in case of massive pending queue if not yet.
author_association: NONE
user: login: raychu86
created_at: 2025-03-17T14:46:28Z
body: > It's more of a concern for future outage, maybe it's helpful to also make sure the validators are able to spread out transactions to different blocks in case of massive pending queue if not yet.

Thank you for bringing up this topic, as it is good to think about in terms of possible edge cases.

 I don't believe this will cause an issue, because this enforcement is on the proposal/certificate level (which are the building components of a block). This just means that each valid certificate would be limited, which implies that a large pending queue will take longer to clear out, but should not invalidate any block production. Blocks can still grow larger if the subdag takes many rounds to commit, however now there's a stronger limiter to prevent blocks from ballooning too quickly.
 
 Another point to note is that block production itself does not care about the number of transactions, it just cares that a proper subdag is found. 
author_association: COLLABORATOR
user: login: zklimaleo
created_at: 2025-03-17T14:48:43Z
body: > > It's more of a concern for future outage, maybe it's helpful to also make sure the validators are able to spread out transactions to different blocks in case of massive pending queue if not yet.
> 
> Thank you for bringing up this topic, as it is good to think about in terms of possible edge cases.
> 
> I don't believe this will cause an issue, because this enforcement is on the proposal/certificate level (which are the building components of a block). This just means that each valid certificate would be limited, which implies that a large pending queue will take longer to clear out, but should not invalidate any block production. Blocks can still grow larger if the subdag takes many rounds to commit, however now there's a stronger limiter to prevent blocks from ballooning too quickly.
> 
> Another point to note is that block production itself does not care about the number of transactions, it just cares that a proper subdag is found.

I see thanks for the clarification!
author_association: NONE
user: login: niklaslong
created_at: 2025-03-17T18:10:31Z
body: Rebased. 
author_association: COLLABORATOR
user: login: niklaslong
created_at: 2025-03-19T15:01:56Z
body: After the recent changes on the VM side, I have moved cost calculation up into the ledger. The two spend limit test cases are expected to fail as no limit is currently enforced.  
author_association: COLLABORATOR
user: login: vicsn
created_at: 2025-03-21T12:03:05Z
body: You can merge in `origin/staging` and open the PR now!
author_association: COLLABORATOR