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

basic chain using tower-abci and penumbra stack #5

Merged
merged 64 commits into from
Jun 15, 2023
Merged

Conversation

noot
Copy link
Collaborator

@noot noot commented May 17, 2023

Implements a basic ABCI application that communicates with a tendermint process. The application only has account logic at this stage, and only one transaction type accounts::transaction::Transaction which represents a value transfer. It uses penumbra_storage::Storage as the state storage, and penumbra_component::Component to define an application component (in this case, there is only the AccountsComponent).

It uses the tower_abci::Service as the base layer which talks to tendermint. The application logic is built up around the requests made by tendermint.

Currently, there are 3 accounts that have a hard-coded balance of 10e18 at genesis: alice, bob, and carol.

Usage:

cd crates/astria-sequencer && cargo build && ../../target/debug/astria-sequencer --genesis-file=test-genesis.json

Clone and build https://github.com/astriaorg/cometbft (make install && make install_abci)

cometbft init
cometbft node

You can use abci-cli to query the application, for example, to query Alice's nonce: (note that the 0xdeadbeef is totally arbitrary, abci-cli requires some hex string as an arg :/)

$ abci-cli query --path=accounts/nonce/alice 0xdeadbeef
I[2023-05-18|22:21:48.105] service start                                module=abci-client msg="Starting socketClient service" impl=socketClient
-> code: OK
-> height: 6
-> key: accounts/nonce/alice
-> key.hex: 6163636F756E74732F6E6F6E63652F616C696365
-> value: 
-> value.hex: 01000000

You can also curl endpoints directly:

$ curl http://localhost:26657/abci_info
{"jsonrpc":"2.0","id":-1,"result":{"response":{"data":"astria_sequencer","version":"0.1.0","app_version":"1","last_block_height":"1"}}}

After node has started, submit a tx:

$ curl http://localhost:26657/broadcast_tx_commit?tx=0x0003000000626f6205000000616c6963651516050000000000000000000000000001000000
{"jsonrpc":"2.0","id":-1,"result":{"check_tx":{"code":0,"data":null,"log":"","info":"","gas_wanted":"0","gas_used":"0","events":[],"codespace":"","sender":"","priority":"0","mempoolError":""},"deliver_tx":{"code":0,"data":null,"log":"","info":"","gas_wanted":"0","gas_used":"0","events":[],"codespace":""},"hash":"5CD86628A864412D505C5B575EE939F7B60AE507119A0F63749AFD2384B8BAEE","height":"8"}}

Note: the tx was generated with the test test_transaction; will need to make a prettier CLI for this

Inside cometbft you'll see the tx was included:

I[2023-05-17|22:06:46.600] received proposal                            module=consensus proposal="Proposal{2/0 (7640BDA8191E35BD9028FE6913D9C831ED095782B087E45C90905FDD476785FA:1:214894D427A1, -1) 94CE5F23AEE1 @ 2023-05-18T02:06:46.598356671Z}"
I[2023-05-17|22:06:46.601] received complete proposal block             module=consensus height=2 hash=7640BDA8191E35BD9028FE6913D9C831ED095782B087E45C90905FDD476785FA
I[2023-05-17|22:06:46.606] finalizing commit of block                   module=consensus height=2 hash=7640BDA8191E35BD9028FE6913D9C831ED095782B087E45C90905FDD476785FA root=35EE717EA57E28688B0DE0653C38247AAD65BF943D00EF3ADAF5023D2F1FF0A2 num_txs=1
I[2023-05-17|22:06:46.698] executed block                               module=state height=2 num_valid_txs=1 num_invalid_txs=0
I[2023-05-17|22:06:51.750] committed state                              module=state height=2 num_txs=1 app_hash=A0E6C663281AB4E36F7DDC647D192D502F2EE481A4CDE6D8C45B2B51658AD7DD
I[2023-05-17|22:06:51.753] indexed block exents                         module=txindex height=2

You'll also see a log inside the sequencer app:

2023-05-19T02:21:50.047549Z  INFO astria_sequencer::app: executed transaction tx=AccountsTransaction(Transaction { to: "bob", from: "alice", amount: 333333, nonce: 1 }) height=8

Query Alice's nonce again

$ abci-cli query --path=accounts/nonce/alice 0x00
I[2023-05-18|22:26:50.206] service start                                module=abci-client msg="Starting socketClient service" impl=socketClient
-> code: OK
-> height: 62
-> key: accounts/nonce/alice
-> key.hex: 6163636F756E74732F6E6F6E63652F616C696365
-> value: 
-> value.hex: 0101000000

Nonce should increase after the tx was sent :D (note the value is borsh-serialized, so doesn't correspond to the raw value exactly)

You can set the path to accounts/balance/(alice, bob, carol) to check balances also

SuperFluffy and others added 10 commits May 16, 2023 15:14
this steps just moves everything out of `sources/` and
places it into `crates/`. `cargo check` passes after
adding making the crates workspace members in the root
`Cargo.toml`.

The crates do not refer to each other by path, and they
are also not yet cleaned up.
Base automatically changed from local_path_dependencies to main May 17, 2023 10:09
@noot noot changed the title [wip] basic chain using tower-abci and penumbra stack basic chain using tower-abci and penumbra stack May 19, 2023
@noot noot marked this pull request as ready for review May 19, 2023 02:38
@noot noot requested a review from SuperFluffy June 9, 2023 18:35
@noot
Copy link
Collaborator Author

noot commented Jun 9, 2023

Big things that I would like to get done before we merge this into main:
no panics except for where invariants are violated (that's the (&mut Arc)::get_mut calls)
error context for all errors through anyhow::Context::context (note that that's different from eyre::WrapErr::wrap_err)
replace manual parsing of primitives by newtype wrappers + deriving borsh or serde (and then use borsh directly or bincode for serde)
instrument all functions

@SuperFluffy I think these are all addressed; the only thing that may not be fully addressed is adding context for every error, as some errors already had context added lower-down on the stack so I wasn't sure if I should re-add another context. let me know if you want literally every error to have additional context and I can add!

state_tx.put_block_height(0);

// call init_chain on all components
AccountsComponent::init_chain(&mut state_tx, &genesis_state).await?;
Copy link
Member

Choose a reason for hiding this comment

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

Should we have some sort of Component manager to define modules and the order they will be called in similar to the Cosmos manager?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the cosmos manager seems more needed because it's an SDK, and it needs to figure out what modules exist at runtime, whereas for us it's fine to just write them in and do it at compile time (this is what penumbra does too)

Ord,
Debug,
)]
pub struct Nonce(u32);
Copy link
Member

Choose a reason for hiding this comment

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

Should we use a u64 here? I believe ethereum uses uint64 for nonce, as does cosmos for sequence?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah, can update, u64 just seemed a bit large lol

Copy link
Member

Choose a reason for hiding this comment

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

Better to plan on being successful than not!

}

#[instrument(skip(self))]
fn put_account_nonce(&mut self, address: &Address, nonce: Nonce) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Do we need a put_account_nonce or should this have all the increment logic and be increment_account_nonce? Is there ever an instance in which account nonce should go up by more than one at a time?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

if we have an increment function then it'll have a get and a put, but usually wherever this function is called there's already a get, so it seems a bit redundant to have 2 gets as we want to minimize reads/writes.

Copy link
Member

Choose a reason for hiding this comment

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

Would we need the get if it was an increment? It seems we are only getting to do the increment.

Ok(())
}

async fn execute<S: StateWriteExt>(&self, state: &mut S) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

Is there anything preventing the call of execute without having first validated the tx? What happens if there are insufficient funds when this is called?

In general I wonder if structurally we should be moving more of the logic of "tx creates these DB level changes" directly into the state implementation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah, this function is only called inside DeliverTx, which always performs validation (stateless and non-stateless) before executing it. what do you mean by moving it directly into the state implementation?

Copy link
Member

Choose a reason for hiding this comment

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

I just want to make sure we are creating hardened code here. Should it be possible to call execute before validation? If no we should enforce that it isn't possible not hope someone doesn't forget that.

Copy link
Member

Choose a reason for hiding this comment

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

We should have a write-up about how state gets transformed when moving through these components.

let from_balance = state.get_account_balance(&self.from).await?;
let from_nonce = state.get_account_nonce(&self.from).await?;
let to_balance = state.get_account_balance(&self.to).await?;
state.put_account_balance(&self.from, from_balance - self.amount)?;
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this is just that lower level, but it feels like maybe the logic for state transitions which has race possibilities should be handled by the state itself? If putting the balance succeeds but nonce fails, we have issues.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

right, so these writes are all happening on a StateDelta, which is basically a list of pending writes that happen during a block's execution. the writes are all batched and only actually written to state once Commit is called. so yeah this effectively is atomic afaik

Copy link
Member

Choose a reason for hiding this comment

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

Can we create an issue to add tests for error cases here?

let from_nonce = state.get_account_nonce(&self.from).await?;
let to_balance = state.get_account_balance(&self.to).await?;
state.put_account_balance(&self.from, from_balance - self.amount)?;
state.put_account_nonce(&self.from, from_nonce + Nonce::from(1))?;
Copy link
Member

Choose a reason for hiding this comment

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

Should we be setting to the nonce of the transaction and validating that it is acceptable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the validation occurs outside this function in check_stateful (which happens right before execute is called) so I don't think we need to double validate

SuperFluffy and others added 3 commits June 14, 2023 19:38
* mention that gcc12 is required

* make items crate-private by default

* add direnv rc to gitignore

* remove dead code and unnecessary test

the tests were essentially checking if the serde derive
was successful

* more expressive naming in genesis state

* straight u128, not f64 cast

* move genesis to its own module

* no error handling in tests

* define an apphash newtype wrapper like in penumbra

* move services to their own module

* WIP

* add tracing to the tower abci server

This commit vendors tracing-tower in the same way as penumbra
has done. It then propagates the tracing spans as needed.

* format tracing-tower

* use penumbra tower trace

* remove to_bytes, from_bytes, rely on borsh

* provide context for everything

* allow configuring listen_addr, genesis state through cli

* clippy suggestion

* rm forgotten uncomment
Copy link
Member

@joroshiba joroshiba left a comment

Choose a reason for hiding this comment

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

LGTM. Great start for the chain overall.

I don't consider any of my comments blocking on this, but we should create issues and consider resolving separately.

@SuperFluffy
Copy link
Member

My last commits fixed compilation in CI. The reason is that CI defines a RUSTFLAGS env var which overrides the setting in .config/cargo.toml.

The other commits are about fixing clippy lints. I also increased the number of lints that are used for astria-sequencer to clippy::pedantic. This is now enforced in a separate step. The other crates will be fixed step by step.

crates/astria-sequencer/src/state_ext.rs Outdated Show resolved Hide resolved
crates/astria-sequencer/src/state_ext.rs Outdated Show resolved Hide resolved
crates/astria-sequencer/src/lib.rs Outdated Show resolved Hide resolved
crates/astria-sequencer/src/service/consensus.rs Outdated Show resolved Hide resolved
@SuperFluffy
Copy link
Member

Big things that I would like to get done before we merge this into main:
no panics except for where invariants are violated (that's the (&mut Arc)::get_mut calls)
error context for all errors through anyhow::Context::context (note that that's different from eyre::WrapErr::wrap_err)
replace manual parsing of primitives by newtype wrappers + deriving borsh or serde (and then use borsh directly or bincode for serde)
instrument all functions

@SuperFluffy I think these are all addressed; the only thing that may not be fully addressed is adding context for every error, as some errors already had context added lower-down on the stack so I wasn't sure if I should re-add another context. let me know if you want literally every error to have additional context and I can add!

To document for posteriority: I added context to literally every error I could find.

tonic = "0.9"
tracing = "0.1"
tracing-subscriber = "0.3"
uuid = "1.3.1"
which = "4.4.0"

[patch."https://github.com/penumbra-zone/penumbra.git"]
penumbra-storage = { git = "https://github.com/astriaorg/penumbra.git", branch = "noot/fix-trace-v0.53.1" }
Copy link
Member

Choose a reason for hiding this comment

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

can you open PRs against penumbra and open an issue here so that we can track it?

Copy link
Member

@SuperFluffy SuperFluffy left a comment

Choose a reason for hiding this comment

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

Approving to unblock this.

Opening an issue to fix rocksdb in CI and docker. See also these github issues:

rust-rocksdb/rust-rocksdb#310 (comment)
rust-rocksdb/rust-rocksdb#354 (comment)

@noot noot merged commit 63ef810 into main Jun 15, 2023
8 checks passed
@noot noot deleted the noot/sequencer branch June 15, 2023 17:41
This pull request was closed.
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.

4 participants