Skip to content

Commit a957416

Browse files
committed
consensus(refactor): use sub_dag index to keep application/consensus in sync
1 parent e777dc7 commit a957416

File tree

10 files changed

+131
-68
lines changed

10 files changed

+131
-68
lines changed

core/application/src/env.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ impl Env<UpdatePerm> {
192192
*/
193193
response.txn_receipts.push(receipt);
194194
}
195-
// Set the last executed block hash
196-
app.set_last_block(block.digest);
195+
// Set the last executed block hash and sub dag index
196+
app.set_last_block(block.digest, block.sub_dag_index);
197197

198198
// Return the response
199199
response

core/application/src/state.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1067,9 +1067,11 @@ impl<B: Backend> State<B> {
10671067
}
10681068

10691069
// This function should only be called in the `run` method on `Env`.
1070-
pub fn set_last_block(&self, block_hash: [u8; 32]) {
1070+
pub fn set_last_block(&self, block_hash: [u8; 32], sub_dag_index: u64) {
10711071
self.metadata
10721072
.set(Metadata::LastBlockHash, Value::Hash(block_hash));
1073+
self.metadata
1074+
.set(Metadata::SubDagIndex, Value::SubDagIndex(sub_dag_index));
10731075
}
10741076

10751077
fn add_service(
@@ -1870,6 +1872,15 @@ impl<B: Backend> State<B> {
18701872
}
18711873
}
18721874

1875+
/// Gets subdag index, returns 0 if not set in state table
1876+
pub fn get_sub_dag_index(&self) -> u64 {
1877+
if let Some(Value::SubDagIndex(value)) = self.metadata.get(&Metadata::SubDagIndex) {
1878+
value
1879+
} else {
1880+
0
1881+
}
1882+
}
1883+
18731884
fn _get_epoch(&self) -> u64 {
18741885
if let Some(Value::Epoch(epoch)) = self.metadata.get(&Metadata::Epoch) {
18751886
epoch

core/application/src/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,7 @@ async fn run_transaction(
10451045
.run(Block {
10461046
transactions: requests,
10471047
digest: [0; 32],
1048+
sub_dag_index: 0,
10481049
})
10491050
.await
10501051
.map_err(|r| anyhow!(format!("{r:?}")))?;

core/consensus/src/edge_node/transaction_store.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,18 @@ impl<T: BroadcastEventInterface<PubSubMsg>> TransactionStore<T> {
178178
while let Some(parcel) = self.get_parcel(&last_digest) {
179179
parcel_chain.push(last_digest);
180180

181-
txn_chain.push_front((parcel.inner.transactions.clone(), last_digest));
181+
txn_chain.push_front((
182+
parcel.inner.transactions.clone(),
183+
parcel.inner.sub_dag_index,
184+
last_digest,
185+
));
182186

183187
if parcel.inner.last_executed == head {
184188
let mut epoch_changed = false;
185189

186190
// We connected the chain now execute all the transactions
187-
for (batch, digest) in txn_chain {
188-
if execution.submit_batch(batch, digest).await {
191+
for (batch, sub_dag_index, digest) in txn_chain {
192+
if execution.submit_batch(batch, digest, sub_dag_index).await {
189193
epoch_changed = true;
190194
}
191195
}

core/consensus/src/execution.rs

Lines changed: 76 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use lightning_interfaces::ExecutionEngineSocket;
99
use lightning_utils::application::QueryRunnerExt;
1010
use narwhal_crypto::DefaultHashFunction;
1111
use narwhal_executor::ExecutionState;
12-
use narwhal_types::{BatchAPI, BatchDigest, ConsensusOutput, Transaction};
12+
use narwhal_types::{Batch, BatchDigest, ConsensusOutput, Transaction};
1313
use serde::{Deserialize, Serialize};
1414
use tokio::sync::{mpsc, Notify};
1515
use tracing::{error, info};
@@ -21,6 +21,7 @@ pub struct AuthenticStampedParcel {
2121
pub transactions: Vec<Transaction>,
2222
pub last_executed: Digest,
2323
pub epoch: Epoch,
24+
pub sub_dag_index: u64,
2425
}
2526

2627
impl ToDigest for AuthenticStampedParcel {
@@ -88,19 +89,32 @@ impl<Q: SyncQueryRunnerInterface, NE: Emitter> Execution<Q, NE> {
8889
}
8990

9091
// Returns true if the epoch changed
91-
pub(crate) async fn submit_batch(&self, payload: Vec<Transaction>, digest: Digest) -> bool {
92+
pub(crate) async fn submit_batch(
93+
&self,
94+
payload: Vec<Transaction>,
95+
digest: Digest,
96+
sub_dag_index: u64,
97+
) -> bool {
9298
let transactions = payload
9399
.into_iter()
94-
.filter_map(|txn| TransactionRequest::try_from(txn.as_ref()).ok())
100+
.filter_map(|txn| {
101+
// Filter out transactions that wont serialize or have already been executed
102+
if let Ok(txn) = TransactionRequest::try_from(txn.as_ref()) {
103+
if !self.query_runner.has_executed_digest(txn.hash()) {
104+
Some(txn)
105+
} else {
106+
None
107+
}
108+
} else {
109+
None
110+
}
111+
})
95112
.collect::<Vec<_>>();
96113

97-
if transactions.is_empty() {
98-
return false;
99-
}
100-
101114
let block = Block {
102-
transactions,
103115
digest,
116+
sub_dag_index,
117+
transactions,
104118
};
105119

106120
let archive_block = block.clone();
@@ -159,62 +173,67 @@ impl<Q: SyncQueryRunnerInterface, NE: Emitter> Execution<Q, NE> {
159173
#[async_trait]
160174
impl<Q: SyncQueryRunnerInterface, NE: Emitter> ExecutionState for Execution<Q, NE> {
161175
async fn handle_consensus_output(&self, consensus_output: ConsensusOutput) {
162-
for (cert, batches) in consensus_output.batches {
163-
let current_epoch = self.query_runner.get_current_epoch();
164-
if cert.epoch() != current_epoch {
165-
// If the certificate epoch does not match the current epoch in the application
166-
// state do not execute this transaction, This could only happen in
167-
// certain race conditions at the end of an epoch and we need this to ensure all
168-
// nodes execute the same transactions
169-
continue;
170-
}
171-
172-
if !batches.is_empty() {
173-
let mut batch_payload =
174-
Vec::with_capacity(batches.iter().fold(0, |acc, batch| acc + batch.size()));
175-
176-
for batch in batches {
177-
for tx_bytes in batch.transactions() {
178-
if let Ok(tx) = TransactionRequest::try_from(tx_bytes.as_ref()) {
179-
if !self.query_runner.has_executed_digest(tx.hash()) {
180-
batch_payload.push(tx_bytes.to_owned());
181-
}
182-
}
183-
}
184-
}
176+
let current_epoch = self.query_runner.get_current_epoch();
185177

186-
if batch_payload.is_empty() {
187-
continue;
178+
let sub_dag_index = consensus_output.sub_dag.sub_dag_index;
179+
println!("subdag index: {sub_dag_index}");
180+
let batch_payload: Vec<Vec<u8>> = consensus_output
181+
.batches
182+
.into_iter()
183+
.filter_map(|(cert, batch)| {
184+
// Skip over the ones that have a different epoch. Shouldnt ever happen
185+
if cert.epoch() != current_epoch {
186+
error!("we recieved a consensus cert from an epoch we are not on");
187+
None
188+
} else {
189+
// Map the batch to just the transactions
190+
Some(
191+
batch
192+
.into_iter()
193+
.flat_map(|batch| match batch {
194+
// work around because batch.transactions() would require clone
195+
Batch::V1(btch) => btch.transactions,
196+
Batch::V2(btch) => btch.transactions,
197+
})
198+
.collect::<Vec<Vec<u8>>>(),
199+
)
188200
}
201+
})
202+
.flatten()
203+
.collect();
189204

190-
// We have batches in the payload send them over broadcast along with an attestion
191-
// of them
192-
let last_executed = self.query_runner.get_last_block();
193-
let parcel = AuthenticStampedParcel {
194-
transactions: batch_payload.clone(),
195-
last_executed,
196-
epoch: current_epoch,
197-
};
198-
199-
let epoch_changed = self.submit_batch(batch_payload, parcel.to_digest()).await;
200-
201-
if let Err(e) = self.tx_narwhal_batches.send((parcel, epoch_changed)).await {
202-
// This shouldn't ever happen. But if it does there is no critical tasks
203-
// happening on the other end of this that would require a
204-
// panic
205-
error!("Narwhal failed to send batch payload to edge consensus: {e:?}");
206-
}
205+
if batch_payload.is_empty() {
206+
return;
207+
}
208+
// We have batches in the payload send them over broadcast along with an attestion
209+
// of them
210+
let last_executed = self.query_runner.get_last_block();
211+
let parcel = AuthenticStampedParcel {
212+
transactions: batch_payload.clone(),
213+
last_executed,
214+
epoch: current_epoch,
215+
sub_dag_index,
216+
};
207217

208-
// Submit the batches to application layer and if the epoch changed reset last
209-
// executed
210-
if epoch_changed {
211-
self.reconfigure_notify.notify_waiters();
212-
}
213-
}
218+
let epoch_changed = self
219+
.submit_batch(batch_payload, parcel.to_digest(), sub_dag_index)
220+
.await;
221+
222+
if let Err(e) = self.tx_narwhal_batches.send((parcel, epoch_changed)).await {
223+
// This shouldn't ever happen. But if it does there is no critical tasks
224+
// happening on the other end of this that would require a
225+
// panic
226+
error!("Narwhal failed to send batch payload to edge consensus: {e:?}");
227+
}
228+
229+
// Submit the batches to application layer and if the epoch changed reset last
230+
// executed
231+
if epoch_changed {
232+
self.reconfigure_notify.notify_waiters();
214233
}
215234
}
216235

217236
async fn last_executed_sub_dag_index(&self) -> u64 {
218-
0
237+
self.query_runner.get_sub_dag_index()
219238
}
220239
}

core/consensus/src/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ fn generate_random_parcel(
4040
transactions,
4141
last_executed: last_executed.unwrap_or([0; 32]),
4242
epoch: 1,
43+
sub_dag_index: 0,
4344
}
4445
}
4546

core/test-utils/src/consensus.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ async fn group_worker(
228228
Block {
229229
transactions: vec![req.unwrap()],
230230
digest: [0; 32],
231+
sub_dag_index: 0
231232
}
232233
},
233234
Some(req) = req_rx.recv() => {
@@ -262,6 +263,7 @@ async fn group_worker(
262263
Block {
263264
transactions: vec![],
264265
digest: [0; 32],
266+
sub_dag_index: 0
265267
}
266268
},
267269
else => {

core/types/src/state.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ pub enum Metadata {
9494
LastEpochHash,
9595
LastBlockHash,
9696
GenesisCommittee,
97+
SubDagIndex,
9798
}
9899

99100
/// The Value enum is a data type used to represent values in a key-value pair for a metadata table
@@ -108,6 +109,7 @@ pub enum Value {
108109
NextNodeIndex(u32),
109110
Hash([u8; 32]),
110111
GenesisCommittee(Vec<NodeIndex>),
112+
SubDagIndex(u64),
111113
}
112114

113115
impl Value {

core/types/src/transaction.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,12 @@ impl From<EthersTransactionWrapper> for EthersTransaction {
8989
/// block.
9090
#[derive(Debug, Clone, Eq, PartialEq)]
9191
pub struct Block {
92-
pub transactions: Vec<TransactionRequest>,
93-
// Digest of the narwhal certificate that included this
92+
/// Digest of the narwhal certificate that included this
9493
pub digest: [u8; 32],
94+
/// The narwhal subdag index that included these transactions
95+
pub sub_dag_index: u64,
96+
/// List of transactions to be executed in this block
97+
pub transactions: Vec<TransactionRequest>,
9598
}
9699

97100
/// An update transaction, sent from users to the consensus to migrate the application
@@ -234,9 +237,14 @@ impl TryFrom<&Block> for Vec<u8> {
234237

235238
fn try_from(value: &Block) -> Result<Self, Self::Error> {
236239
let mut bytes = Vec::new();
240+
// First 32 bytes are digest
237241
bytes.extend_from_slice(&value.digest);
242+
// Next 8 bytes are the subdag index
243+
bytes.extend_from_slice(&value.sub_dag_index.to_le_bytes());
244+
// Next 8 bytes are the number of transactions that are following
238245
let num_txns = value.transactions.len() as u64;
239246
bytes.extend_from_slice(&num_txns.to_le_bytes());
247+
// the rest of the bytes are the transactions
240248
for tx in &value.transactions {
241249
// TODO(matthias): would be good to serialize to borrowed bytes here instead
242250
let tx_bytes: Vec<u8> = tx.try_into()?;
@@ -253,9 +261,14 @@ impl TryFrom<Vec<u8>> for Block {
253261

254262
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
255263
let digest: [u8; 32] = value.get(0..32).context("Out of bounds")?.try_into()?;
256-
let num_txns_bytes: [u8; 8] = value.get(32..40).context("Out of bounds")?.try_into()?;
264+
265+
let sub_dag_bytes = value.get(32..40).context("Out of bounds")?.try_into()?;
266+
let sub_dag_index = u64::from_le_bytes(sub_dag_bytes);
267+
268+
let num_txns_bytes: [u8; 8] = value.get(40..48).context("Out of bounds")?.try_into()?;
257269
let num_txns = u64::from_le_bytes(num_txns_bytes);
258-
let mut pointer = 40;
270+
271+
let mut pointer = 48;
259272
let mut transactions = Vec::with_capacity(num_txns as usize);
260273
for _ in 0..num_txns {
261274
let tx_len_bytes: [u8; 8] = value
@@ -273,8 +286,9 @@ impl TryFrom<Vec<u8>> for Block {
273286
pointer += tx_len;
274287
}
275288
Ok(Block {
276-
transactions,
277289
digest,
290+
sub_dag_index,
291+
transactions,
278292
})
279293
}
280294
}

core/utils/src/application.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ pub trait QueryRunnerExt: SyncQueryRunnerInterface {
115115
}
116116
}
117117

118+
/// Returns the current sub dag index
119+
fn get_sub_dag_index(&self) -> u64 {
120+
if let Some(Value::SubDagIndex(value)) = self.get_metadata(&Metadata::SubDagIndex) {
121+
value
122+
} else {
123+
0
124+
}
125+
}
126+
118127
/// Returns a full copy of the entire node-registry,
119128
/// Paging Params - filtering nodes that are still a valid node and have enough stake; Takes
120129
/// from starting index and specified amount.

0 commit comments

Comments
 (0)