Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor node from 4 to 1 binary #323

Merged
merged 23 commits into from
Apr 18, 2024
Merged

Refactor node from 4 to 1 binary #323

merged 23 commits into from
Apr 18, 2024

Conversation

phklive
Copy link
Contributor

@phklive phklive commented Apr 15, 2024

In this PR I propose the refactor of the node internal structure to enable/disable services using flags and reduce from 4 to 1 binary.

closes: #92 #322

@phklive phklive changed the base branch from main to next April 15, 2024 17:34
@phklive phklive marked this pull request as ready for review April 16, 2024 12:00
Copy link
Contributor

@polydez polydez left a comment

Choose a reason for hiding this comment

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

Looks good to me, thank you! I've left a few inline comments. The main idea here is that we should move config parsing and service starting functionality to the node binary (leave only serve in the libraries). We also should union top-level configs in one single structure in the node binary with optional field for each corresponding service:

struct Config {
    rpc: Option<RpcConfig>,
    faucet: Option<FaucetConfig>,
   ....
}

miden-node-rpc = { path = "../../crates/rpc", version = "0.2" }
miden-node-store = { path = "../../crates/store", version = "0.2" }
miden-node-utils = { path = "../../crates/utils", version = "0.2" }
miden-node-faucet = { path = "../../crates/faucet", version = "0.1" }
Copy link
Contributor

Choose a reason for hiding this comment

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

I would probably move these local dependencies to workspace's Cargo.toml and specify here just workspace = true. And we don't need to specify versions for them.

// MAIN FUNCTION
// =================================================================================================

pub async fn start_block_producer(config_filepath: &Path) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think, we should move such functions from libraries to binary and get rid of anyhow dependency in libraries. server::serve can return tonic::transport::Error. We also should process config file once in the node binary, not in each library.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you explain where in the binary you would add such functions?

We have 4 for now:

  • start_block_producer
  • start_rpc
  • start_store
  • start_faucet

Also to note that the faucet is not a Tonic server but an Actix web server.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain where in the binary you would add such functions?

I think, commands/start would be a good place for them.

Also to note that the faucet is not a Tonic server but an Actix web server.

Not a problem, it can return Actix's status messages. The idea is to keep flexible errors in the binary, but return error codes from the library.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Understood. Changed.


/// Faucet top-level configuration.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize)]
pub struct FaucetTopLevelConfig {
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be substituted by single node's config structure with optional elements for block-producer, store, faucet and rpc. Config of each service (library) stays in the library.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed


/// Faucet top-level configuration.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize)]
pub struct FaucetTopLevelConfig {
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be substituted by single node's config structure with optional elements for block-producer, store, faucet and rpc. Config struct of each service (library) stays in the library.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

path = "src/main.rs"
bench = false
doctest = false

[features]
tracing-forest = ["miden-node-utils/tracing-forest"]

[dependencies]
anyhow = { version = "1.0" }
Copy link
Contributor

Choose a reason for hiding this comment

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

We should get rid of anyhow in libraries.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you explain further why we should remove it in the libraries and not in the binary?

Copy link
Contributor

Choose a reason for hiding this comment

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

Using anyhow or eyre in libraries is considered as a bad practice, because it's hard to handle different errors by library users. But binary is a different case: we usually just print error message from the binary to the user and don't need to handle it, so anyhow is suitable there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Understood.

// MAIN FUNCTION
// =================================================================================================

pub async fn start_rpc(config_filepath: &Path) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Move this functionality to the binary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

// MAIN FUNCTION
// =================================================================================================

pub async fn start_store(config_filepath: &Path) -> Result<()> {
Copy link
Contributor

@polydez polydez Apr 16, 2024

Choose a reason for hiding this comment

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

We should move this functionality to the binary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

Comment on lines 86 to 90
let bytes = match created_notes.first() {
Some(note) => {
note_id = note.id();
note.to_bytes()
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Have you tested this on the client? I think in order to import this you might need an InputNoteRecord.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, fixed.

@phklive phklive requested review from polydez and igamigo April 17, 2024 13:44
@phklive
Copy link
Contributor Author

phklive commented Apr 17, 2024

Fixed all requested changes and comments.

I have used .expect quite extensively considering that multiple states are irrecoverable ( example if we load the configuration file incorrectly or if the server fails to run ) let me know what you think about it or if you have a better solution in mind.

Now the commands used to run the node are the following:

cargo run --release --features testing start node (or faucet, store, block-producer, rpc. You can also pass the --config flag to start to specify a specific config file usually located in bin/node/miden-node.toml)

Copy link
Contributor

@polydez polydez left a comment

Choose a reason for hiding this comment

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

Looks good, thanks! I've added several inline comments here.

Cargo.toml Outdated
@@ -25,6 +26,13 @@ exclude = [".github/"]
[workspace.dependencies]
miden-air = { version = "0.9", default-features = false }
miden-lib = { version = "0.2"}
miden-node-block-producer = { path = "crates/block-producer", version = "0.2" }
Copy link
Contributor

Choose a reason for hiding this comment

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

I think, it's not needed to specify version here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed.


pub async fn start_node(config: NodeConfig) -> Result<()> {
let mut join_set = JoinSet::new();
let db = Db::setup(config.store.clone().expect("Missing store configuration.")).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rather not panic here, but raise error with message:

let db = Db::setup(config.store.clone().context("Missing store configuration.")?).await?;

.await
},
StartCommand::Rpc => {
start_rpc(config.rpc.expect("Missing rpc configuration.")).await
Copy link
Contributor

@polydez polydez Apr 17, 2024

Choose a reason for hiding this comment

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

The same as before, I prefer to raise errors, not panics. But for binaries it's not a mandatory.

@@ -56,9 +55,11 @@ pub async fn serve(config: BlockProducerConfig) -> Result<()> {

let addr = config
.endpoint
.to_socket_addrs()?
.to_socket_addrs()
.expect("Failed to turn address into socket address.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Please, avoid panics in the library code. Consider adding new error enumeration, if this function can return different error types.

Copy link
Collaborator

@igamigo igamigo left a comment

Choose a reason for hiding this comment

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

This LGTM in general. I'm a bit confused with some of the error handling changes on component code but once that's handled it should be good to merge. Another thing that we might want to look at in the future is to have top-level commands for each component instead of them being a subcommand of the start (ie, faucet start instead of start faucet; could allow for more component-specific commands in the future), but probably not many good reasons right now to do so.

repository.workspace = true

[features]
# Makes `make-genesis` subcommand run faster. Is only suitable for testing.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's document (here and elsewhere) that this should be set to match the node's configuration as well

Comment on lines 60 to 65
// Instantiate transaction request
let tx_request = client
.lock()
.await
.build_transaction_request(tx_template)
.expect("Failed to build transaction request.");
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 ideally we would not panic on any of the faucet's code

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How do you handle situations like missing config file or others? It is going to panic at one point right?

Copy link
Collaborator

@igamigo igamigo Apr 17, 2024

Choose a reason for hiding this comment

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

In general I think handling error is better overall, although I agree that missing config files would be an anomaly. However what I meant is that in this scenario specifically where we are handling HTTP requests, there should ideally be no Rust panics but rather the server should return the HTTP error code that best describes the situation. In this case the transaction template to transaction request conversion should work fine but it does rely on various validations, etc. that could fail and so the code should ideally not panic.

Comment on lines +87 to +100
// Serialize note into bytes
let bytes = match created_notes.first() {
Some(note) => {
note_id = note.id();
InputNoteRecord::from(note.clone()).to_bytes()
},
None => {
return Err(
FaucetError::InternalServerError("Failed to generate note.".to_string()).into()
)
},
};

info!("A new note has been created: {}", note_id);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks good, were you able to test this with the client?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it works.

.next()
.ok_or(anyhow!("Couldn't resolve server address"))?;
.expect("Failed to resolve address.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure I agree with changing these errors into expect()s for these services

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, @polydez requested to remove the anyhow inside the libs. Looking into a better way to handle errors atm.

@phklive phklive requested a review from polydez April 17, 2024 16:10
Copy link
Contributor

@polydez polydez left a comment

Choose a reason for hiding this comment

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

I'd like to improve error representation here:

  1. Use thiserror for all error enumerations and write Display representation for each error variant.
  2. Try to keep correct error chain without converting concrete types to String. Sometimes, String is also okay, but not for cases when we have a single error structure/enumeration which caused the error.

I've hilighted some of examples inline.

ApiInitialisationFailed(String),
ApiServeFailed(String),
AddressResolutionFailed(String),
EndpointToSocketFailed(String),
Copy link
Contributor

Choose a reason for hiding this comment

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

We can use io::Error here instead of String.

DatabaseConnectionFailed(String),
}

impl std::fmt::Display for ApiError {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use thiserror for ApiError and write display implementation using its macro. We use intensively thiserror, for example: https://github.com/0xPolygonMiden/miden-node/blob/next/block-producer/src/errors.rs#L16-L47

pub enum ApiError {
ApiInitialisationFailed(String),
ApiServeFailed(String),
AddressResolutionFailed(String),
Copy link
Contributor

@polydez polydez Apr 17, 2024

Choose a reason for hiding this comment

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

Currently this String is used only for error message about address resolution fail. It would be better to not to provide String here, but output error message for this variant in Display implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

The issue is still here. It would be better to just remove this String argument.

@@ -0,0 +1,14 @@
#[derive(Debug)]
pub enum ApiError {
ApiInitialisationFailed(String),
Copy link
Contributor

Choose a reason for hiding this comment

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

Here would be better to keep tonic::Error, don't need to convert it to string.

#[derive(Debug)]
pub enum ApiError {
ApiInitialisationFailed(String),
ApiServeFailed(String),
Copy link
Contributor

Choose a reason for hiding this comment

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

The same as above.

@phklive phklive requested a review from polydez April 18, 2024 09:25
@phklive phklive requested a review from bobbinth April 18, 2024 09:56

pub async fn start_node(config: NodeConfig) -> Result<()> {
let mut join_set = JoinSet::new();
let db = Db::setup(config.store.clone().context("Missing store configuration.")?).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rather spawn start_* functions here. This would help us to reduce/simplify code.


#[derive(Debug, Error)]
pub enum FaucetError {
/// Client has submitted a bad request
Copy link
Contributor

Choose a reason for hiding this comment

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

I think, these docs are excessive here.

pub enum ApiError {
ApiInitialisationFailed(String),
ApiServeFailed(String),
AddressResolutionFailed(String),
Copy link
Contributor

Choose a reason for hiding this comment

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

The issue is still here. It would be better to just remove this String argument.

@phklive
Copy link
Contributor Author

phklive commented Apr 18, 2024

#323 (comment)

Which errors are you talking about? All of them or specifically this one?

@polydez
Copy link
Contributor

polydez commented Apr 18, 2024

#323 (comment)

Which errors are you talking about? All of them or specifically this one?

Just the pointed error variant. If you look at the code which raises this error, you will see that this string initialized with error message similar to what is implemented in Display for this enum variant:

.ok_or("Failed to resolve address.")

I'd probably set this string with endpoint.to_string() or Debug representation of endpoint in order to give information to the user about which endpoint was failed to convert.

@phklive phklive requested a review from polydez April 18, 2024 13:18
Copy link
Contributor

@polydez polydez left a comment

Choose a reason for hiding this comment

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

Looks good, thanks! I've left a couple of nit comments.

// block on all tasks
while let Some(res) = join_set.join_next().await {
// For now, if one of the components fails, crash the node
res.unwrap().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think, it would be better to raise error here:

res??;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

.map_err(ApiError::EndpointToSocketFailed)?
.next()
.ok_or(config.endpoint.to_string())
.map_err(|err| ApiError::AddressResolutionFailed(err))?;
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 like such chained conversion here. We firstly convert None to Err("https://host:port") and then wrap it with ApiError::AddressResolutionFailed.

It would be better to just do:

    let addr = config
        .endpoint
        .to_socket_addrs()
        .map_err(ApiError::EndpointToSocketFailed)?
        .next()
        .ok_or_else(|| ApiError::AddressResolutionFailed(config.endpoint.to_string()))?;

Copy link
Contributor

Choose a reason for hiding this comment

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

The second disadvantage of current solution is that we format endpoint as string even if no errors occurred.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@phklive phklive requested a review from polydez April 18, 2024 13:39
Copy link
Contributor

@polydez polydez left a comment

Choose a reason for hiding this comment

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

Looks good to me, thank you!

@phklive phklive merged commit 51f6a54 into next Apr 18, 2024
5 checks passed
@phklive phklive deleted the phklive-reduce-binaries branch April 18, 2024 14:31
Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks great! Thank you! I know this has already been merged, but I left a few small comments. Most of these can be separate issues which we can address in future PRs.

Comment on lines 42 to 56
To install for production use cases, run:

```sh
cargo install --path node
cargo install --path bin/node
```

This will install the executable `miden-node` in your PATH, at `~/.cargo/bin/miden-node`.

Otherwise, if only to try the node out for testing, run:

```sh
cargo install --features testing --path node
cargo install --features testing --path bin/node
```

Currently, the only difference between the two is how long the `make-genesis` command will take to run (see next subsection).
Copy link
Contributor

Choose a reason for hiding this comment

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

Is cargo install --path bin/node still the right command to use? We can now install directly from crates.io and so I think this could be cargo install miden-node.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would be the right command to use in case you would want to run the software using the cloned repo.

If you would like to use the binary directly installed from crates.io then yes indeed cargo install miden-node would be the right command.

I believe that both command could / should be documented for completeness ( having multiple ways to download / install the software )

Let me know what is your pov.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think for cargo install downloading from crates.io is probably the main use case - but I'm not against also having a section which describes how to deploy a node by cloning the repo. In general, we should probably update the section about deploying the node because we now have multiple ways (installing from crates.io, via docker, via .deb package, by cloning the repo) and we should describe in a more comprehensive way than we do now.

Comment on lines +29 to +35
miden-node-block-producer = { path = "crates/block-producer" }
miden-node-faucet = { path = "crates/faucet" }
miden-node-proto = { path = "crates/proto" }
miden-node-rpc = { path = "crates/rpc" }
miden-node-store = { path = "crates/store" }
miden-node-test-macro = { path = "crates/test-macro" }
miden-node-utils = { path = "crates/utils" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we not include version here as well? For example:

miden-node-block-producer = { path = "crates/block-producer", version = "0.2" }

I think the version is needed for crates published to crates.io (but I'm not 100% sure).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe that the version is inferred from the workspace cargo.toml that has the version number added.

bin/node/miden-node.toml Show resolved Hide resolved
crates/rpc/src/config.rs Show resolved Hide resolved
bin/node/miden-node.toml Show resolved Hide resolved
bobbinth pushed a commit that referenced this pull request Apr 24, 2024
* New working structure

* Faucet available but note generation broken stalls on input_note

* Fix faucet with new client note handling

* Lints + added comments and small refactors

* Removed anyhow from librairies + moved start fns to binary

* Refactored config location

* Removed top level config from libs + removed redundant tests

* Added config.rs

* Added start_faucet + FaucetConfig to binary

* Fixed not serialisation issue

* Modified cargo.toml's to include workspace deps

* Added optionality to the configuration parameters

* Added error handling / removed expect and panic

* Improved error handling with thiserror

* Fixed docker build and run

* Update documentation after refactor

* Updated start_node to use start_*

* Removed irrelevant comments for errors

* Made error printing more expressive using Endpoint

* Added in store and block-producer

* lint

* Fixed nits
polydez added a commit that referenced this pull request Apr 27, 2024
* fix: use `OutputNote::metadata` and remove `TODO` (#320)

* refactor: store migrations into SQL files

* formatting: reduce linebreaks

* fix: update SQL file after rebasing

* feat: implement database settings storage

* feat: compare migrations by hashes

* formatting: rustfmt

* Improve CI & builds (#325)

* Improved workflows

* Added spacing

* Refactor node from 4 to 1 binary (#323)

* New working structure

* Faucet available but note generation broken stalls on input_note

* Fix faucet with new client note handling

* Lints + added comments and small refactors

* Removed anyhow from librairies + moved start fns to binary

* Refactored config location

* Removed top level config from libs + removed redundant tests

* Added config.rs

* Added start_faucet + FaucetConfig to binary

* Fixed not serialisation issue

* Modified cargo.toml's to include workspace deps

* Added optionality to the configuration parameters

* Added error handling / removed expect and panic

* Improved error handling with thiserror

* Fixed docker build and run

* Update documentation after refactor

* Updated start_node to use start_*

* Removed irrelevant comments for errors

* Made error printing more expressive using Endpoint

* Added in store and block-producer

* lint

* Fixed nits

* fix: remove table constant

* feat: add check for changing of schema version outside migrations

* feat: settings in human-readable format, improve logging

* docs: address review comments

* refactor: use `ANY` datatype in settings table with `STRICT`

---------

Co-authored-by: Paul-Henry Kajfasz <[email protected]>
bobbinth pushed a commit that referenced this pull request Apr 29, 2024
* fix: use `OutputNote::metadata` and remove `TODO` (#320)

* refactor: store migrations into SQL files

* formatting: reduce linebreaks

* fix: update SQL file after rebasing

* feat: implement database settings storage

* feat: compare migrations by hashes

* formatting: rustfmt

* Improve CI & builds (#325)

* Improved workflows

* Added spacing

* Refactor node from 4 to 1 binary (#323)

* New working structure

* Faucet available but note generation broken stalls on input_note

* Fix faucet with new client note handling

* Lints + added comments and small refactors

* Removed anyhow from librairies + moved start fns to binary

* Refactored config location

* Removed top level config from libs + removed redundant tests

* Added config.rs

* Added start_faucet + FaucetConfig to binary

* Fixed not serialisation issue

* Modified cargo.toml's to include workspace deps

* Added optionality to the configuration parameters

* Added error handling / removed expect and panic

* Improved error handling with thiserror

* Fixed docker build and run

* Update documentation after refactor

* Updated start_node to use start_*

* Removed irrelevant comments for errors

* Made error printing more expressive using Endpoint

* Added in store and block-producer

* lint

* Fixed nits

* fix: remove table constant

* feat: add check for changing of schema version outside migrations

* feat: settings in human-readable format, improve logging

* docs: address review comments

* refactor: use `ANY` datatype in settings table with `STRICT`

---------

Co-authored-by: Paul-Henry Kajfasz <[email protected]>
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.

feature flags to disable/enable services
4 participants