Skip to content
Merged
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
25 changes: 25 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 @@ -109,6 +109,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 @@ -117,6 +118,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
41 changes: 41 additions & 0 deletions bin/funding-service/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[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 }
axum = { workspace = true }
backon = { workspace = true }
clap = { features = ["env", "string"], workspace = true }
humantime = { workspace = true }
miden-node-proto = { workspace = true }
miden-node-tracing = { workspace = true }
miden-node-utils = { workspace = true }
miden-protocol = { features = ["std"], workspace = true }
serde = { workspace = true }
tokio = { features = ["macros", "net", "rt-multi-thread", "sync", "time"], workspace = true }
tower-http = { features = ["timeout"], workspace = true }
url = { workspace = true }

[dev-dependencies]
miden-protocol = { features = ["std", "testing"], workspace = true }
miden-standards = { workspace = true }
rand = { workspace = true }
rand_chacha = { workspace = true }
tempfile = { workspace = true }
tower = { features = ["util"], workspace = true }
20 changes: 20 additions & 0 deletions bin/funding-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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, so a restart needs no recovery. Only the
account file, which holds the account ID and its signing key, is on disk.

The service reads the chain's protocol configuration from the node at startup, together with the genesis block header.

The service serves a JSON HTTP API. `GET /status` reports the funding account, its balance, and the block that balance
was read at. An operator alerts on that balance, because the service does not refill itself.

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

## License

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

use std::path::Path;

use anyhow::{Context, Result};
use miden_protocol::account::{AccountFile, AccountId, AccountType};

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

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

impl FunderKey {
/// Reads the funding account 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 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",
Comment on lines +26 to +27

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.

Perhaps a question whether we should be doing this or not 🤔 I guess the value owned would be small..

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.

One thing about limiting the service to public accounts is that it simplifies operations. The service does not even need to keep track of the account itself, just the private key. So I would say if the service is limited to public accounts only, scrap any functionality that tries to keep and validate the local state of the account versus the state provided by the network, and just rely on the latter. Only if the service supports private accounts should you take care of keeping the local state valid.

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.

It does simplify things, also means it easy to check if its out of funds via the explorer.

account.id(),
);

Ok(Self { account_id: account.id() })
}

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

#[cfg(test)]
mod tests {
use miden_protocol::ONE;
use miden_protocol::account::auth::{AuthScheme, AuthSecretKey};
use miden_protocol::account::{Account, AccountType};
use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey;
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)],
);

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

assert_eq!(funder.account_id(), account.id());
}

/// 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