Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@
- Fixed no-std compatibility for remote prover clients ([#1407](https://github.com/0xMiden/miden-node/pull/1407)).
- Fixed `AccountProofRequest` to retrieve the latest known state in case specified block number (or chain tip) does not contain account updates ([#1422](https://github.com/0xMiden/miden-node/issues/1422)).
- Fixed missing asset setup for full account initialization ([#1461](https://github.com/0xMiden/miden-node/pull/1461)).
- Fixed validator to use pre-stored asset witnesses from the transaction inputs instead of trying to open the partial vault ([#1490](https://github.com/0xMiden/miden-node/pull/1490)).
- Fixed `GetNetworkAccountIds` pagination to return the chain tip ([#1489](https://github.com/0xMiden/miden-node/pull/1489)).
- Fixed validator to use pre-stored asset witnesses from the transaction inputs instead of trying to open the partial vault ([#1490](https://github.com/0xMiden/miden-node/pull/1490)).
- Fixed the network monitor counter account to use the storage slot name ([#1492](https://github.com/0xMiden/miden-node/pull/1492)).

## v0.12.6

Expand Down
10 changes: 5 additions & 5 deletions bin/network-monitor/src/assets/counter_program.masm
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Counter program for network monitoring with note authentication
# Storage layout:
# - Slot 0: counter value (u64)
# - Slot 1: authorized wallet account id as [prefix, suffix, 0, 0]
# - OWNER_SLOT: authorized wallet account id as [prefix, suffix, 0, 0]
# - COUNTER_SLOT: counter value (u64)

use miden::core::sys
use miden::protocol::active_account
Expand All @@ -11,14 +11,14 @@ use miden::protocol::account_id
use miden::protocol::tx


# The slot in this component's storage layout where the counter is stored.
const COUNTER_SLOT = word("miden::monitor::counter_contract::counter")
const OWNER_SLOT = word("miden::monitor::counter_contract::owner")

# Increment function with note authentication
# => []
pub proc increment
# Ensure the note sender matches the authorized wallet stored in slot 1.
push.1 exec.active_account::get_item
# Ensure the note sender matches the authorized wallet.
push.OWNER_SLOT[0..2] exec.active_account::get_item
# => [owner_prefix, owner_suffix, 0, 0]

exec.active_note::get_sender
Expand Down
17 changes: 12 additions & 5 deletions bin/network-monitor/src/counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use tracing::{error, info, instrument, warn};

use crate::COMPONENT;
use crate::config::MonitorConfig;
use crate::deploy::counter::COUNTER_SLOT_NAME;
use crate::deploy::{MonitorDataStore, create_genesis_aware_rpc_client, get_counter_library};
use crate::status::{
CounterTrackingDetails,
Expand Down Expand Up @@ -83,7 +84,7 @@ async fn get_genesis_block_header(rpc_client: &mut RpcClient) -> Result<BlockHea
Ok(block_header)
}

/// Fetch the latest nonce of the given account from RPC.
/// Fetch the latest counter value of the given account from RPC.
async fn fetch_counter_value(
rpc_client: &mut RpcClient,
account_id: AccountId,
Expand All @@ -95,9 +96,13 @@ async fn fetch_counter_value(
let account = Account::read_from_bytes(&raw)
.map_err(|e| anyhow::anyhow!("failed to deserialize account details: {e}"))?;

let storage_slot = account.storage().slots().first().expect("storage slot is always value");
let word = storage_slot.value();
let value = word.as_elements().last().expect("a word is always 4 elements").as_int();
// Access the counter slot by name to avoid index-ordering issues
let word = account
.storage()
.get_item(&COUNTER_SLOT_NAME)
.context("failed to get counter storage slot")?;

let value = word.as_elements().first().expect("a word is always 4 elements").as_int();

Ok(Some(value))
} else {
Expand Down Expand Up @@ -644,14 +649,16 @@ async fn create_and_submit_network_note(

let final_account = executed_tx.final_account().clone();

let tx_inputs = executed_tx.tx_inputs().to_bytes();

// Prove the transaction
let prover = LocalTransactionProver::default();
let proven_tx = prover.prove(executed_tx).context("Failed to prove transaction")?;

// Submit the proven transaction
let request = ProvenTransaction {
transaction: proven_tx.to_bytes(),
transaction_inputs: None,
transaction_inputs: Some(tx_inputs),
};

let block_height: BlockNumber = rpc_client
Expand Down
4 changes: 2 additions & 2 deletions bin/network-monitor/src/deploy/counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ use tracing::instrument;

use crate::COMPONENT;

static OWNER_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
pub static OWNER_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::monitor::counter_contract::owner")
.expect("storage slot name should be valid")
});

static COUNTER_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
pub static COUNTER_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::monitor::counter_contract::counter")
.expect("storage slot name should be valid")
});
Expand Down
4 changes: 3 additions & 1 deletion bin/network-monitor/src/deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,15 @@ pub async fn deploy_counter_account(counter_account: &Account, rpc_url: &Url) ->
.await
.context("Failed to execute transaction")?;

let tx_inputs = executed_tx.tx_inputs().to_bytes();

let prover = LocalTransactionProver::default();

let proven_tx = prover.prove(executed_tx).context("Failed to prove transaction")?;

let request = ProvenTransaction {
transaction: proven_tx.to_bytes(),
transaction_inputs: None,
transaction_inputs: Some(tx_inputs),
};

rpc_client
Expand Down
7 changes: 4 additions & 3 deletions crates/ntx-builder/src/actor/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,10 @@ impl NtxContext {
let notes = notes.into_iter().map(Note::from).collect::<Vec<_>>();
let (successful, failed) = self.filter_notes(&data_store, notes).await?;
let executed = Box::pin(self.execute(&data_store, successful)).await?;
let tx_inputs = executed.tx_inputs().clone();
let proven = Box::pin(self.prove(executed.into())).await?;
let tx_id = proven.id();
self.submit(proven).await?;
self.submit(proven, &tx_inputs).await?;
Ok((tx_id, failed))
}
.in_current_span()
Expand Down Expand Up @@ -258,9 +259,9 @@ impl NtxContext {

/// Submits the transaction to the block producer.
#[instrument(target = COMPONENT, name = "ntx.execute_transaction.submit", skip_all, err)]
async fn submit(&self, tx: ProvenTransaction) -> NtxResult<()> {
async fn submit(&self, tx: ProvenTransaction, tx_inputs: &TransactionInputs) -> NtxResult<()> {
self.block_producer
.submit_proven_transaction(tx)
.submit_proven_transaction(tx, tx_inputs)
.await
.map_err(NtxError::Submission)
}
Expand Down
4 changes: 2 additions & 2 deletions crates/ntx-builder/src/actor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ impl AccountActor {
let chain_state = self.chain_state.read().await.clone();
// Find a candidate transaction and execute it.
if let Some(tx_candidate) = state.select_candidate(crate::MAX_NOTES_PER_TX, chain_state) {
self.execute_transactions(&mut state, tx_candidate).await;
Box::pin(self.execute_transactions(&mut state, tx_candidate)).await;
} else {
// No transactions to execute, wait for events.
self.mode = ActorMode::NoViableNotes;
Expand Down Expand Up @@ -281,7 +281,7 @@ impl AccountActor {
self.script_cache.clone(),
);

let execution_result = context.execute_transaction(tx_candidate).await;
let execution_result = Box::pin(context.execute_transaction(tx_candidate)).await;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need Box::pin() here? And same question for line 250 above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was related to the changes that I did on Context::execute_transaction, but since we are rolling back that, the box pin aren't needed anymore.

match execution_result {
// Execution completed without failed notes.
Ok((tx_id, failed)) if failed.is_empty() => {
Expand Down
5 changes: 3 additions & 2 deletions crates/ntx-builder/src/block_producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use miden_node_proto::domain::mempool::MempoolEvent;
use miden_node_proto::generated::{self as proto};
use miden_node_utils::FlattenResult;
use miden_protocol::block::BlockNumber;
use miden_protocol::transaction::ProvenTransaction;
use miden_protocol::transaction::{ProvenTransaction, TransactionInputs};
use miden_tx::utils::Serializable;
use tokio_stream::StreamExt;
use tonic::Status;
Expand Down Expand Up @@ -45,10 +45,11 @@ impl BlockProducerClient {
pub async fn submit_proven_transaction(
&self,
proven_tx: ProvenTransaction,
tx_inputs: &TransactionInputs,
) -> Result<(), Status> {
let request = proto::transaction::ProvenTransaction {
transaction: proven_tx.to_bytes(),
transaction_inputs: None,
transaction_inputs: Some(tx_inputs.to_bytes()),
};
Comment on lines 45 to 53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed: block producer ignores this field. Here is how it is supposed to work:

  • This transaction_inputs field is the optional field that we currently use for guardrails.
  • It is used in the RPC to prepare the request and send the request to the validator.
  • Once the transaction is OKed by the validator, the RPC forwards the transaction to the block producer, but the transaction_inputs are no longer needed.


self.client.clone().submit_proven_transaction(request).await?;
Expand Down
30 changes: 17 additions & 13 deletions crates/validator/src/tx_validation/data_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,24 @@ impl DataStore for TransactionInputsDataStore {
});
}

Result::<Vec<_>, _>::from_iter(vault_keys.into_iter().map(|vault_key| {
match self.tx_inputs.account().vault().open(vault_key) {
Ok(vault_proof) => {
AssetWitness::new(vault_proof.into()).map_err(|err| DataStoreError::Other {
error_msg: "failed to open vault asset tree".into(),
source: Some(err.into()),
let stored_witnesses = self.tx_inputs.asset_witnesses();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this will work. As discussed in another comment, this will only cover the pre-fetched witnesses. As @igamigo mentioned, we should be able to get rid of this field entirely but the change would need to happen in miden-base. @igamigo, could you create an issue for this?

For broader context, this should be properly addressed with #1493.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll close this PR then, and open another one only with the changes for the network monitor

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opend #1501 with the standing changes

Copy link
Collaborator

@igamigo igamigo Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, I believe this does fix the issue (and I think @SantiagoPittella did some tests to confirm this). This covers the pre-fetched witnesses only, but AFAICT (might be missing something so take it with a grain of salt), this is enough because on re-execution that's the only time this will be called, as all other witnesses get loaded from advice data directly and do not go through the DataStore first. My comment was more pointing out the inconsistency and the fact that this did not seem to be very documented.

But going to open an issue for potentially removing the field in miden-base so it can be discussed there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pre-fetched asset witnesses will cover witness of asset of all input notes. So, for transactions that only consume notes, this will be fine. But I don't think these witness will cover assets for output note. These are generated during transaction execution and so, are not added to this filed.


vault_keys
.into_iter()
.map(|vault_key| {
stored_witnesses
.iter()
.find(|w| w.authenticates_asset_vault_key(vault_key))
.cloned()
.ok_or_else(|| DataStoreError::Other {
error_msg: format!(
"asset witness not found for vault key {vault_key}",
)
.into(),
source: None,
})
},
Err(err) => Err(DataStoreError::Other {
error_msg: "failed to open vault".into(),
source: Some(err.into()),
}),
}
}))
})
Comment on lines +77 to +93
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK this change is fine because tx_inputs.asset_witnesses() only contains prefetched assets. On re-execution, get_vault_asset_witnesses() would only run for prefetched assets anyway since TransactionExecutor prefetches regardless of context and anything “dynamic” would otherwise be loaded from advice data directly.

cc @PhilippGackstatter is there a reason we keep prefetched asset witnesses separate? If not, could they be removed TransactionInputs? Then either:

  • the executor can check whether the prefetched assets are already present in the advice map (and avoid the data store call if so), or maybe
  • we can add a specific executor entrypoint/flag that skips prefetching because the VM will load them from the advice map

If not, I think the behavior of get_vault_asset_witnesses() only being called for prefetched assets when re-executing might be worth documenting (unless it already is somewhere) because it can be misleading at first sight.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original motivation is described here: 0xMiden/miden-base#2113 (comment)

Now that we have TransactionAdviceInputs in miden-protocol, at first glance, it looks like we could revisit this and add asset witnesses directly to TransactionInputs::advice_inputs in TransactionInputs::with_asset_witnesses.
Is this what you mean? If yes, then I can open an issue for this in base.

I think the behavior of get_vault_asset_witnesses() only being called for prefetched assets when re-executing might be worth documenting (unless it already is somewhere) because it can be misleading at first sight.

I understand the cause for confusion. The behavior, that some data is added to advice inputs after execution, seems like a property of the TransactionExecutor. So, would it be sufficient to add a doc section on re-execution on TransactionExecutor that describes what data fetched from DataStore is added to the advice inputs after execution? This would then serve as a reference for this kind of question and we could note that this implies certain methods may not be called during re-execution.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iiuc then the change above relies on the fact that the provided TransactionInputs is the one used to actually execute the ProvenTransaction? And that execution adds additional data to "inputs", making it an execution output artifact?

If so, then this is definitely confusing and will lead to issues since this is passed in by users.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what you mean?

I think so, but not sure if that is sufficient. What I'm mainly trying to get at is that it seems that on the re-execution case, dynamic calls to DataStore::get_vault_asset_witnesses() are basically skipped because the Merkle paths are already in the advice data (because you are taking the inputs from an ExecutedTransaction). This does not seem to be the case for the prefetched assets call to the DataStore, as it's called every time even if the data is already in the advice inputs. Additionally, only the prefetched asset witnesses are part of TransactionInputs separately. This means the validator implementor needs to know this DataStore call will only be called necessarily once in the whole transaction for the prefetched assets (and now that I see the comment you linked it makes sense why this was the case), and this is what I'd document more.
Alternatively, it would be great if the asset witnesses prefetch is skipped on re-execution as well. Then if you get a call to DataStore::get_vault_asset_witnesses() during re-execution it's basically an error every time and there's no ambiguity; also the implementation itself becomes trivial and can maybe be provided from miden-base. But maybe I'm missing something else here.

My assumptions (some of which might be incorrect) are:

  • On dynamic calls to the DataStore (i.e., not the prefetch of prepare_tx_inputs()), the host first checks that the Merkle path is not already on the advice data.
    • If it is, it's loaded from there and the call is not done
  • When creating inputs from an ExecutedTransaction for the re-execution case, you take the latest state of the advice data (this includes added witnesses)
  • Prefetched asset witnesses are retrieved every time regardless of them being in the advice inputs, and later get loaded again into the Merkle store

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we merge this PR?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these changes are still valid, so I’m okay with merging this. Though I still have the concerns I mentioned above.

.collect()
}
}

Expand Down