Skip to content

feat(network-monitor): add fees support - #2526

Merged
SantiagoPittella merged 10 commits into
nextfrom
santiagopittella-add-fees-to-monitor
Sep 4, 2026
Merged

feat(network-monitor): add fees support#2526
SantiagoPittella merged 10 commits into
nextfrom
santiagopittella-add-fees-to-monitor

Conversation

@SantiagoPittella

Copy link
Copy Markdown
Collaborator

Closes #2450

Summary

On fee-charging chains the monitor now funds its accounts from the faucet (reusing the existing PoW token-request flow) and keeps itself funded:

  • Wallet and counter are funded with public P2ID faucet notes, consumed as unauthenticated input notes by each account's first transaction (input-note assets land before the epilogue withdraws the fee, so empty-vault accounts bootstrap themselves).
  • The counter prices the increment note at the per-transaction fee bound, so every increment attaches a FEE_SPONSORSHIP note that pays for the network transaction consuming it. It also allowlists the sponsorship and P2ID scripts and carries BasicWallet (as does the wallet, to consume P2ID notes).
  • The wallet's fee balance is tracked, shown on the dashboard, and topped up automatically from the faucet when it runs low. A failed top-up flips the card unhealthy.
  • The fee asset is callback-enabled, so every transaction moving it must provision the issuing faucet as a foreign account. The monitor fetches the faucet's full state and witness at the reference block.
  • The remote-prover probe funds its payload the same way, but does not require re-funding since it never actually spends it. A fee-charging chain without --faucet-url fails the monitor at startup

Mainnet, which has no faucet, is a follow-up.

Also in this PR:

  • Genesis now pre-funds the native faucet's own vault with 1,000 MIDEN. Mint requests execute as network transactions paying fees from the faucet's vault, which was empty, so minting deadlocked on any fee-charging chain.
  • Bumped the miden protocol dependencies from 0.16.0-rc.4 to 0.16.0-rc.6 (version pins only, no code changes needed).
  • Zero-fee chains keep today's exact behavior.

TEsted it against a live local stack (fee-enabled genesis, base fee 500) with the faucet from 0xMiden/faucet#289: funding, deployment, increments with sponsorships, and automatic top-up all worked end to end.

Changelog

[[entry]]
scope       = "network-monitor"
impact      = "added"
description = "Support fee-charging chains: the monitor funds its accounts from the faucet, attaches fee sponsorships to increment notes, and tops its balance up automatically."

[[entry]]
scope       = "node"
impact      = "fixed"
description = "Pre-fund the generated native faucet's own vault at genesis so its mint network transactions can pay fees."

[[entry]]
scope       = "general"
impact      = "changed"
description = "Bump miden protocol dependencies to 0.16.0-rc.6."

@SantiagoPittella
SantiagoPittella force-pushed the santiagopittella-add-fees-to-monitor branch from b5e9f07 to 41c0222 Compare August 28, 2026 23:12
@igamigo
igamigo self-requested a review August 31, 2026 03:12
@igamigo

igamigo commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Genesis now pre-funds the native faucet's own vault with 1,000 MIDEN. Mint requests execute as network transactions paying fees from the faucet's vault, which was empty, so minting deadlocked on any fee-charging chain.

I think this should not be needed - instead, the sponsorship note should pay for the network transactions. These notes are created by operator account which should have tokens already. Two things to keep in mind here:

  • Mint and burn notes are both (expectedly) priced at 0 (link). By default a client will not pay for such notes because they would be free, but for the native faucet we want to do it anyway as it needs to pay for the transaction itself. I think this is what may have prompted you to add this workaround
  • However, another actual blocker which we have to fix separately is that we were setting an incorrect fee asset ID for the native faucet (link, we call out in the TODO the protocol issue/PR we needed; this is what we need to address). This would ideally not be a problem because the actual fees are 0, but the NTX builder will not pick up these notes and even if it did, the executor would error out because the asset ID did not match the expected one.
    • I think the referenced protocol PR here has not actually landed on any RC release, so it'd need to be done or we would have to basically duplicate the code on the node.

cc @Mirko-von-Leipzig @mmagician

@mmagician

Copy link
Copy Markdown
Contributor

Mint and burn notes are both (expectedly) priced at 0 (link).

This works now, but it will need to change at the point when batch builders' tx acceptance criteria change and they require fee payment.

I think the referenced protocol PR here has not actually landed on any RC release, so it'd need to be don

Thanks, good catch! I created a v0.16.0-rc-7 release now, I think that PR just missed the rc-6 release earlier.

@igamigo

igamigo commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Mint and burn notes are both (expectedly) priced at 0 (link).

This works now, but it will need to change at the point when batch builders' tx acceptance criteria change and they require fee payment.

Even though the notes are currently priced at 0 and the batch builders will accept transactions with 0 fees, the current situation is that pay_fees does happen unconditionally, which means we either:

  • Have assets in the faucet's vault. I don't think we want that based on some previous discussions, but not 100% sure. This is what this PR resorted to. Eventually the network account would run out of faucets in this scenario
  • Price notes at 0 and from the operator, still send sponsorship notes that have enough assets to cover the transaction. We'd rely on the NTX builder still trying to pair the network note with it (which I think it does)

@SantiagoPittella
SantiagoPittella force-pushed the santiagopittella-add-fees-to-monitor branch from 41c0222 to dc2daab Compare August 31, 2026 15:25
@SantiagoPittella

Copy link
Copy Markdown
Collaborator Author

I moved the deps bump and genesis changes to #2538 , which will be ready to merge after the fix of the protocol releases.

Comment thread bin/network-monitor/src/deploy/counter.rs Outdated
Comment thread bin/network-monitor/src/deploy/mod.rs

@igamigo igamigo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looking good. Leaving some general comments/quetsions for now.

let mut fee_schedule = vec![(increment_script.root(), increment_note_fee)];

if verification_base_fee > 0 {
allowed_scripts.insert(FeeSponsorshipNote::script_root());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: I think this is not nedeed if you use AuthNetworkAccount::new/NetworkAccount::builder because it's already included by default

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Using AuthNetworkAccount::new is actually a bit (not much really) more lines than the custom approach due to AccountConfigure. This particular line that you mentioned was already refactored.

.into();
let increment_note_fee = AssetAmount::new(max_fee_per_transaction(verification_base_fee))
.expect("the per-transaction fee bound fits an asset amount");
let mut fee_schedule = vec![(increment_script.root(), increment_note_fee)];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: I think the whole function would be a bit more readable if we were using more concrete types instead of having mut variables, (e.g., BasicConstantFeePolicy::new().with_fee(...), etc.)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Continuing with this, would be nice to have an CounterNetworkAcc type or something similar that generalizes this logic. This can also have different initializers depending on the network (e.g., CounterNetworkAcc::new_with_fees(...))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Continuing with this, would be nice to have an CounterNetworkAcc type or something similar that generalizes this logic. This can also have different initializers depending on the network (e.g., CounterNetworkAcc::new_with_fees(...))

This is no longer needed since I removed the branching.

nit: I think the whole function would be a bit more readable if we were using more concrete types instead of having mut variables, (e.g., BasicConstantFeePolicy::new().with_fee(...), etc.)

Replaced the mut with:

let fee_schedule = [
    (increment_script.root(), increment_note_fee),
    (FeeSponsorshipNote::script_root(), AssetAmount::ZERO),
    (P2idNote::script_root(), AssetAmount::ZERO),
];
let allowed_scripts = BTreeSet::from(fee_schedule.map(|(root, _)| root));

allowed_scripts.insert(FeeSponsorshipNote::script_root());
fee_schedule.push((FeeSponsorshipNote::script_root(), AssetAmount::ZERO));
allowed_scripts.insert(P2idNote::script_root());
fee_schedule.push((P2idNote::script_root(), AssetAmount::ZERO));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need P2ID support? I think fee sponsorship notes should be enough to cover the ntx fees

@igamigo igamigo Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah actually I think we need it for deploying the account and not necessarily for the network txs, right? This happened in the client integration tests and I think the solution was similar. I meant to ask @mmagician or maybe @partylikeits1983 if there were any other simpler ways to deploy network accounts.

To state the problem clearly: the developer of the account needs to deploy the network account through a transaction, and this transaction needs to pay for fees. You can't use a sponsorship note because it needs to be tied to a feature note, which means your best option (other than developing a new, custom note) is to add a basic wallet + P2ID support for the network account. Is this expected? Seems a bit overkill for only one transaction, but maybe the expectation is that all accounts should do this regardless. If this is the case, then I think the P2ID note root should be allowed by default in the auth component (as done with the other script roots). cc @bobbinth

EDIT: Actually thinking more about this, in this case we should be able to consume an increment counter note plus its FeeSponsorshipNote? But those funds need to come from somewhere.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I tried it and it worked. It makes a bit more complex the deployment and messes a bit with the counter which makes us to offset in some places. If you think that the approach is worth anyways I can push it.

Comment on lines +118 to +120
if verification_base_fee > 0 {
builder = builder.with_component(BasicWallet);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Similar to the other comment, I don't think we need wallet support in the account. If for some reason we do need it, I think it'd be fine to always include the component

Comment on lines 105 to 110
self.client.clone(),
self.rpc_url.clone(),
self.funding.clone(),
self.interval,
self.probe_tx.clone(),
self.name.clone(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I was following where the FeeFunding was being created to be passed to the services and realized that it seems like mostly all of these parameters are derived from MonitorConfig. The new FeeFunding parameter goes through a lot of functions too. Could these functions just take MonitorConfig instead? Or maybe there could be different config structs derived for each service.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I like this and it is correct, but we probably need to do it as a follow up and apply the same pattern in all the monitor tasks, since this is a repeated pattern. Wdyt?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sounds good!

Comment thread bin/network-monitor/src/funding.rs Outdated

/// Requests fee tokens from the faucet for the monitor's accounts.
#[derive(Clone, Debug)]
pub struct FeeFunding {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: Would FaucetClient describe this better?

Comment thread bin/network-monitor/src/faucet.rs Outdated
Comment on lines +163 to +170
let last_error = match request_tokens(
&self.client,
&self.url,
&self.account_id,
MINT_AMOUNT,
self.solve_timeout,
)
.await

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could this use the FeeFunding code instead?

Comment thread bin/network-monitor/src/funding.rs Outdated
Comment on lines +110 to +116
pub async fn fund(
&self,
rpc_client: &mut RpcClient,
account_id: AccountId,
fee_faucet_id: AccountId,
amount: u64,
) -> Result<Note> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I wonder if fee_faucet_id could be a field of FeeFunding and it could do the asset ID check internally. Similarly with teh rpc_client it could be nice if the FeeFunding struct could have its own instance, but maybe this does not work for some other reason.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

FaucetClient stays as the HTTP client and added FeeFunder to bind it to the RPC client and the genesis fee faucet ID, so fund() now takes just an account and an amount

@Mirko-von-Leipzig Mirko-von-Leipzig left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not a solid review from me; but I think perhaps that's okay, its not super critical. We'll pick up issues I'm sure.

@kkovaacs kkovaacs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good to me!

@igamigo igamigo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

@SantiagoPittella
SantiagoPittella merged commit 4c211fc into next Sep 4, 2026
32 checks passed
@SantiagoPittella
SantiagoPittella deleted the santiagopittella-add-fees-to-monitor branch September 4, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add fee support for network monitor

5 participants