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
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ reason = "Internal change only."
Do not add an entry for a protocol, Rust MSRV, or database migration version update. Release notes
derive these updates from repository files.

Allowed scopes: rpc, docs, node, note-transport, network-monitor, ntx-builder, prover, validator, internal, general
Allowed scopes: rpc, docs, node, note-transport, network-monitor, funding-service, ntx-builder, prover, validator, internal, general
Allowed impacts: breaking, added, changed, fixed, removed, deprecated
-->

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ jobs:
{"component":"ntx-builder", "bin":"miden-ntx-builder", "port":50301, "target":"runtime"},
{"component":"remote-prover", "bin":"miden-remote-prover", "port":50051, "target":"runtime"},
{"component":"network-monitor", "bin":"miden-network-monitor", "port":3000, "target":"runtime"},
{"component":"funding-service", "bin":"miden-funding-service", "port":50401, "target":"runtime"},
{"component":"node-tps-benchmark", "bin":"miden-benchmark", "target":"runtime-tool"}
]
'
Expand Down Expand Up @@ -306,6 +307,8 @@ jobs:
MIDEN_REMOTE_PROVER_IMAGE: ${{ env.REGISTRY_PREFIX }}/miden-remote-prover:${{ needs.preflight.outputs.tag }}
MIDEN_NETWORK_MONITOR_IMAGE:
${{ env.REGISTRY_PREFIX }}/miden-network-monitor:${{ needs.preflight.outputs.tag }}
MIDEN_FUNDING_SERVICE_IMAGE:
${{ env.REGISTRY_PREFIX }}/miden-funding-service:${{ needs.preflight.outputs.tag }}
MIDEN_BENCHMARK_IMAGE: ${{ env.REGISTRY_PREFIX }}/miden-node-tps-benchmark:${{ needs.preflight.outputs.tag }}
with:
compose-file: docker-compose.yml
Expand Down Expand Up @@ -395,6 +398,8 @@ jobs:
${{ env.REGISTRY_PREFIX }}/miden-remote-prover:${{ needs.preflight.outputs.immutable_tag }}
MIDEN_NETWORK_MONITOR_IMAGE:
${{ env.REGISTRY_PREFIX }}/miden-network-monitor:${{ needs.preflight.outputs.immutable_tag }}
MIDEN_FUNDING_SERVICE_IMAGE:
${{ env.REGISTRY_PREFIX }}/miden-funding-service:${{ needs.preflight.outputs.immutable_tag }}
MIDEN_BENCHMARK_IMAGE:
${{ env.REGISTRY_PREFIX }}/miden-node-tps-benchmark:${{ needs.preflight.outputs.immutable_tag }}
with:
Expand Down
31 changes: 31 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"bin/benchmark",
"bin/funding-service",
"bin/large-account-benchmark",
"bin/network-monitor",
"bin/node",
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ RUN --mount=type=cache,sharing=locked,id=cargo-registry-${TARGETARCH},target=/us
--bin miden-note-transport \
--bin miden-ntx-builder \
--bin miden-network-monitor \
--bin miden-funding-service \
--bin miden-remote-prover \
--bin miden-benchmark && \
mkdir -p /app/bin && \
Expand All @@ -128,6 +129,7 @@ RUN --mount=type=cache,sharing=locked,id=cargo-registry-${TARGETARCH},target=/us
/app/target/release/miden-note-transport \
/app/target/release/miden-ntx-builder \
/app/target/release/miden-network-monitor \
/app/target/release/miden-funding-service \
/app/target/release/miden-remote-prover \
/app/target/release/miden-benchmark \
/app/bin/ && \
Expand Down
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ install-note-transport: ## Installs note transport
install-ntx-builder: ## Installs ntx-builder
cargo install --path bin/ntx-builder --locked

.PHONY: install-funding-service
install-funding-service: ## Installs funding service
cargo install --path bin/funding-service --locked

.PHONY: install-remote-prover
install-remote-prover: ## Install remote prover's CLI
cargo install --path bin/remote-prover --bin miden-remote-prover --locked
Expand Down Expand Up @@ -216,6 +220,7 @@ docker-build: docker-build-node \
docker-build-note-transport \
docker-build-ntx-builder \
docker-build-monitor \
docker-build-funding-service \
docker-build-remote-prover \
docker-build-benchmark

Expand Down Expand Up @@ -285,6 +290,19 @@ docker-build-monitor: ## Builds the network monitor using Docker
--build-arg PORT=3000 \
-t miden-network-monitor .

.PHONY: docker-build-funding-service
docker-build-funding-service: ## Builds the funding service using Docker
@CREATED=$$(date -u +'%Y-%m-%dT%H:%M:%SZ') && \
VERSION="$(DOCKER_VERSION)" && \
COMMIT=$$(git rev-parse HEAD) && \
$(DOCKER_COMMAND) build $(DOCKER_PULL_ARG) $(DOCKER_PLATFORM_ARG) \
--build-arg CREATED="$$CREATED" \
--build-arg VERSION="$$VERSION" \
--build-arg COMMIT="$$COMMIT" \
--build-arg BIN=miden-funding-service \
--build-arg PORT=50401 \
-t miden-funding-service .

.PHONY: docker-build-remote-prover
docker-build-remote-prover: ## Builds the remote prover using Docker
@CREATED=$$(date -u +'%Y-%m-%dT%H:%M:%SZ') && \
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ A quick overview of the binaries:
blocks.
- [`network-monitor`](./bin/network-monitor/README.md): a tool which monitors a network's infrastructure, e.g. block
production, RPC, validator, prover, faucet, explorer, and note transport.
- [`funding-service`](./bin/funding-service/README.md): sends the chain's native asset to any account which asks for it,
so infrastructure can pay transaction fees.

There are additional binaries but they're more supplementary; see their READMEs for more information.

Expand Down
49 changes: 49 additions & 0 deletions bin/funding-service/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[package]
authors.workspace = true
description = "Miden funding service"
edition.workspace = true
homepage.workspace = true
keywords = ["funding", "miden"]
license.workspace = true
name = "miden-funding-service"
readme = "README.md"
repository.workspace = true
rust-version.workspace = true
version.workspace = true

[lints]
workspace = true

[lib]
doctest = false

[dependencies]
anyhow = { workspace = true }
backon = { workspace = true }
clap = { features = ["env", "string"], workspace = true }
futures = { workspace = true }
hex = { workspace = true }
humantime = { workspace = true }
miden-node-proto = { workspace = true }
miden-node-proto-build = { workspace = true }
miden-node-tracing = { workspace = true }
miden-node-utils = { workspace = true }
miden-protocol = { features = ["std"], workspace = true }
miden-standards = { workspace = true }
miden-tx = { features = ["concurrent", "std"], workspace = true }
rand = { workspace = true }
rand_chacha = { workspace = true }
thiserror = { workspace = true }
tokio = { features = ["macros", "net", "rt-multi-thread", "sync", "time"], workspace = true }
tokio-stream = { features = ["net"], workspace = true }
tonic = { workspace = true }
tonic-health = { workspace = true }
tonic-reflection = { workspace = true }
tower-http = { workspace = true }
url = { workspace = true }

[dev-dependencies]
miden-protocol = { features = ["std", "testing"], workspace = true }
miden-testing = { workspace = true }
miden-tx = { features = ["concurrent", "std", "testing"], workspace = true }
tempfile = { workspace = true }
24 changes: 24 additions & 0 deletions bin/funding-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Miden funding service

`miden-funding-service` is a Miden node binary that sends the chain's native asset to any account that asks for it.

## Operation

The service holds no chain state. It reads the funding account from the node before every transaction, so a restart
needs no recovery. Only the account file, which holds the account ID and its signing key, is on disk.

Each request creates a private pay-to-ID note for the requested account. The service waits until the note is committed
in a block, then returns the note together with proof of its inclusion.

The account is refilled by sending it a public pay-to-ID note that holds the native asset. The service scans for those
notes and consumes them on its own.

The `Status` endpoint reports the funding account, its balance, and the block that balance was read at. An operator
alerts on that balance, because the service never mints.

The service does not authenticate requests. An operator must restrict access to its gRPC API at the infrastructure
level.

## License

This project is [MIT licensed](../../LICENSE).
143 changes: 143 additions & 0 deletions bin/funding-service/src/account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//! Loading of the funding account.

use std::path::Path;

use anyhow::{Context, Result};
use miden_protocol::Word;
use miden_protocol::account::auth::AuthSecretKey;
use miden_protocol::account::{AccountFile, AccountId, AccountType};
use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey;

// FUNDER KEY
// ================================================================================================

/// The identity of the funding account, loaded from its account file.
#[derive(Clone, Debug)]
pub struct FunderKey {
account_id: AccountId,
secret_key: SecretKey,
code_commitment: Word,
}

impl FunderKey {
/// Reads the funding account and its signing key from an account file.
pub fn load(path: &Path) -> Result<Self> {
let account_file = AccountFile::read(path)
.with_context(|| format!("failed to read the account file at {}", path.display()))?;

let secret_key = account_file
.auth_secret_keys
.iter()
.find_map(|key| match key {
AuthSecretKey::Falcon512Poseidon2(secret_key) => Some(secret_key.clone()),
_ => None,
})
.with_context(|| {
format!(
"the account file at {} holds no Falcon512Poseidon2 secret key",
path.display()
)
})?;

let account = account_file.account;
anyhow::ensure!(
account.id().account_type() == AccountType::Public,
"the funding account {} is not public: the service reads its state from the node, \
which only stores the full state of a public account",
account.id(),
);

Ok(Self {
account_id: account.id(),
secret_key,
code_commitment: account.code().commitment(),
})
}

pub fn account_id(&self) -> AccountId {
self.account_id
}

pub fn secret_key(&self) -> &SecretKey {
&self.secret_key
}

/// The commitment to the account code in the account file.
///
/// Compared against the code of the account on chain, so an account file from another network
/// fails at startup instead of as an opaque execution error.
pub fn code_commitment(&self) -> Word {
self.code_commitment
}
}

#[cfg(test)]
mod tests {
use miden_protocol::ONE;
use miden_protocol::account::auth::AuthScheme;
use miden_protocol::account::{Account, AccountType};
use miden_standards::account::auth::Approver;
use miden_standards::account::wallets::create_basic_wallet;
use rand::{RngExt, SeedableRng};
use rand_chacha::ChaCha20Rng;

use super::*;

/// Builds a wallet the way the genesis configuration does, so the test covers the file the
/// service actually loads.
fn genesis_wallet(account_type: AccountType) -> (Account, SecretKey) {
let mut rng = ChaCha20Rng::from_seed([7; 32]);
let secret_key = SecretKey::with_rng(&mut rng);
let auth = Approver::new(secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2);
let init_seed: [u8; 32] = rng.random();
let mut account =
create_basic_wallet(init_seed, auth, account_type).expect("wallet should build");
account.set_nonce(ONE).expect("nonce should be settable");
(account, secret_key)
}

fn write_account_file(
dir: &Path,
account: &Account,
keys: Vec<AuthSecretKey>,
) -> std::path::PathBuf {
let path = dir.join("funding_service.mac");
AccountFile::new(account.clone(), keys)
.write(&path)
.expect("file should be written");
path
}

#[test]
fn loads_a_public_wallet_with_its_key() {
let dir = tempfile::tempdir().unwrap();
let (account, secret_key) = genesis_wallet(AccountType::Public);
let path = write_account_file(
dir.path(),
&account,
vec![AuthSecretKey::Falcon512Poseidon2(secret_key.clone())],
);

let funder = FunderKey::load(&path).expect("a public wallet with a key should load");

assert_eq!(funder.account_id(), account.id());
assert_eq!(funder.code_commitment(), account.code().commitment());
assert_eq!(funder.secret_key().public_key(), secret_key.public_key());
}

/// The service reads the funder's vault from the node, which is only possible for a public
/// account.
#[test]
fn rejects_a_private_account() {
let dir = tempfile::tempdir().unwrap();
let (account, secret_key) = genesis_wallet(AccountType::Private);
let path = write_account_file(
dir.path(),
&account,
vec![AuthSecretKey::Falcon512Poseidon2(secret_key)],
);

let err = FunderKey::load(&path).expect_err("a private account must be rejected");
assert!(err.to_string().contains("is not public"), "unexpected error: {err}");
}
}
Loading
Loading