Skip to content

Comments

feat(bridge-withdrawer): PoC astria-bridge-withdrawer implementation#984

Merged
noot merged 41 commits intomainfrom
itamarreif/bridge-scaffolding
Jun 1, 2024
Merged

feat(bridge-withdrawer): PoC astria-bridge-withdrawer implementation#984
noot merged 41 commits intomainfrom
itamarreif/bridge-scaffolding

Conversation

@itamarreif
Copy link
Contributor

@itamarreif itamarreif commented Apr 19, 2024

Summary

implement ethereum and submitter modules for astria-bridge-withdrawer.

Background

required to implement rollup bridge withdrawals.

Changes

ethereum:

  • implement AstriaWithdrawer.sol contract, put inside ethereum/ dir as a foundry project
  • implement ethereum module which contains a Watcher that watches the contract for Withdrawal events
  • implement event to action conversion
  • the events are sent to a Batcher which batches all events by block number and sends a batch of actions to a sequencer handler

sequencer:

  • submit txs to sequencer when a batch of actions is received

Testing

unit tests

manual testing:

  1. run sequencer+cometbft as normal (just run + just run-cometbft in astria-sequencer)
  2. run anvil as normal (anvil in a terminal)
  3. init the bridge account:
export SEQUENCER_PRIVATE_KEY=2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90
./target/debug/astria-cli sequencer init-bridge-account --sequencer-url=http://localhost:26657 --rollup-name=astria --sequencer.chain-id=astria
  1. just copy-env in astria-bridge-withdrawer, put the private key from above into a file, update .env to use that
  2. go to astria-bridge-withdrawer/ethereum, cp local.env.example .env
  3. deploy the withdrawer contract:
source .env
forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY src/AstriaWithdrawer.sol:AstriaWithdrawer
  1. run this twice, makes 2 withdrawals (forces anvil to make 2 blocks) note: the withdrawer will only submit the withdraw for the first tx, as it's waiting for the next block before submitting the second and anvil doesn't auto-build blocks by default
forge script script/AstriaWithdrawer.s.sol:AstriaWithdrawerScript \
   --rpc-url $RPC_URL --broadcast --sig "withdrawToSequencer()" -vvvv
  1. in astria-bridge-withdrawer, update ASTRIA_BRIDGE_WITHDRAWER_ETHEREUM_CONTRACT_ADDRESS to be the contract address deployed in step 6.
  2. just run and you should see a withdrawal being submitted:
2024-05-24T19:36:22.952210Z DEBUG astria_bridge_withdrawer::withdrawer::submitter: fetched latest nonce nonce=1
2024-05-24T19:36:23.003380Z DEBUG astria_bridge_withdrawer::withdrawer::submitter: signed transaction tx_hash=faad567d4f71ba20bdd9a1f679c4c3875c4b172c96d236155d52eb7ca02cd389
2024-05-24T19:36:23.003742Z DEBUG submit_tx: astria_bridge_withdrawer::withdrawer::submitter: submitting signed transaction to sequencer nonce=1 transaction.hash=faad567d4f71ba20bdd9a1f679c4c3875c4b172c96d236155d52eb7ca02cd389
2024-05-24T19:36:24.049943Z  INFO astria_bridge_withdrawer::withdrawer::submitter: withdraw batch successfully executed. sequencer.block=881 sequencer.tx_hash=FAAD567D4F71BA20BDD9A1F679C4C3875C4B172C96D236155D52EB7CA02CD389 rollup.height=2

Related Issues

#913

closes #1109

@itamarreif itamarreif self-assigned this Apr 19, 2024
@noot noot changed the title bridge binary scaffolding feat(bridge-withdrawer): begin astria-bridge-withdrawer implementation May 21, 2024
## Summary
implement `ethereum` module for astria-bridge-withdrawer.

## Background
required to read from the astria withdrawal evm contract.

## Changes
- implement `AstriaWithdrawer.sol` contract, put inside `ethereum/` dir
as a foundry project
- implement `ethereum` module which contains a `Watcher` that watches
the contract for `Withdrawal` events
- the events are sent to a `Batcher` which batches all events by block
number and sends a batch of events to a sequencer handler (to be
implemented later)

## Testing
unit tests

## Related Issues

 #913
@github-actions github-actions bot added the ci issues that are related to ci and github workflows label May 22, 2024
noot and others added 2 commits May 23, 2024 16:55
…er and ics20 withdrawals (#1101)

## Summary
as title says

## Changes
- update AstriaWithdrawer contract to have separate methods for
sequencer and ics20 withdrawals `withdrawToSequencer` or
`withdrawToOriginChain`
- these methods now take different parameters and emit different events

## Testing
unit tests
)

## Summary
This adds the `Submitter` and merges in the `Watcher` related logic into
a single `WithdrawService`.

`Submitter` receives batches of rollup transactions that have been
converted into sequencer `Action`s from the `Batcher` and does the
following:
1. fetch the current nonce and create the `UnsignedTransaction`
2. sign the transaction
3. `broadcast_tx_commit`

If submission to the sequencer fails for any reason (either in `CheckTx`
or `DeliverTx`), the `Submitter` will stop, instead relying on the
service's recovery process to reconstruct the batch from rollup
transaction and resubmit.

## Background
Withdrawal batches need to be submitted to the sequencer.

## Changes
- Add `Submitter`
- Add logic for converting the events reaped from rollup to `Action`s
- a lot of random cleanup

## Testing
How are these changes tested?

## Metrics
- Some metrics we use in composer already since i copied over a good
amount of submission logic:
    - NONCE_FETCH_COUNT
    - NONCE_FETCH_FAILURE_COUNT
    - NONCE_FETCH_LATENCY
    - CURRENT_NONCE
    - SEQUENCER_SUBMISSION_FAILURE_COUNT
    - SEQUENCER_SUBMISSION_LATENCY

## Related Issues
Link any issues that are related, prefer full github links.

closes <!-- list any issues closed here -->

---------

Co-authored-by: elizabeth <elizabethjbinks@gmail.com>
@noot noot changed the title feat(bridge-withdrawer): begin astria-bridge-withdrawer implementation feat(bridge-withdrawer): PoC astria-bridge-withdrawer implementation May 24, 2024
@github-actions github-actions bot added conductor pertaining to the astria-conductor crate sequencer-relayer pertaining to the astria-sequencer-relayer crate composer pertaining to composer labels May 24, 2024
@noot noot marked this pull request as ready for review May 24, 2024 01:34
@noot noot requested review from a team, SuperFluffy, joroshiba and noot as code owners May 24, 2024 01:34
Copy link
Contributor

@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.

I have skimmed the PR and left a few early comments.

Please provide blackbox tests for this service.

/// It also contains a `return_address` field which may or may not be the same as the signer
/// of the packet. The funds will be returned to the `return_address` in the case of a timeout.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
Copy link
Contributor

Choose a reason for hiding this comment

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

All fields should be private.

Copy link
Contributor

Choose a reason for hiding this comment

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

all the other actions have pub fields - we should probably make constructors for them instead? but a bit out of scope for here

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 make a follow up issue here? seems a general code quality thing

Copy link
Contributor

Choose a reason for hiding this comment

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

Gonna let it pass here because the other actions also have their fields exposed. But yeah, in general I prefer to not leak that stuff. We should address this in a follow, prior to publishing this crate.

/// Construct a `SequencerSigner` from a file.
///
/// The file should contain a hex-encoded ed25519 secret key.
pub(super) fn from_path<P: AsRef<Path>>(path: P) -> eyre::Result<Self> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pub(super) fn from_path<P: AsRef<Path>>(path: P) -> eyre::Result<Self> {
pub(super) fn try_from_path<P: AsRef<Path>>(path: P) -> eyre::Result<Self> {

};
use sequencer_client::Address;

pub(super) struct SequencerSigner {
Copy link
Contributor

Choose a reason for hiding this comment

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

This type does not in fact sign anything as far as I can see? I would expect there to be a method SequencerSigner::sign.

mod signer;

pub(super) struct Handle {
pub(super) batches_tx: mpsc::Sender<Batch>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Make this private. No point having a handle if it just reveals its contents. Also, please don't overload abbreviations. In other parts of this crate _tx is used as transaction, whereas here it's used as sending


impl Builder {
/// Instantiates an `Submitter`.
pub(crate) async fn build(self) -> eyre::Result<(super::Submitter, super::Handle)> {
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. There are multiple errors in here that don't have context
  2. This constructor should ideally not be async

let sequencer_cometbft_client = sequencer_client::HttpClient::new(&*cometbft_endpoint)
.context("failed constructing cometbft http client")?;

let actual_chain_id = get_sequencer_chain_id(sequencer_cometbft_client.clone()).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

This could probably be moved into a "runtime intializer" that runs before the select! loop

This would remove the need to make this method async.

@noot noot requested a review from SuperFluffy May 28, 2024 16:30
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.

Some general comments on source of withdrawal contract code, only change I really want to see on PR though is refactoring the github ci test logic. I want to be able to isolate the tests using all the foundry stuff in case it fails on us again.

Copy link
Member

Choose a reason for hiding this comment

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

is this a generated file? Should it be excluded from the repo in .gitignore? Or added to the generated files list?

Copy link
Contributor

Choose a reason for hiding this comment

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

this is generated by foundry - however we need it in the astria_withdrawer.rs module to do abigen for the contract, so i committed it

@@ -0,0 +1,3 @@
[submodule "crates/astria-bridge-withdrawer/ethereum/lib/forge-std"]
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 the contracts outside of crates in the monorepo perhaps?

@@ -0,0 +1,20 @@
# anvil account 0
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Copy link
Member

Choose a reason for hiding this comment

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

How are these env vars used with contract deployment? Is this foundry env var? Generally we want to put private keys in files.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. We should never leak private keys in plain text.

Copy link
Contributor

Choose a reason for hiding this comment

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

this is anvil account 0 so it's a known private key. i can remove tho

# The socket address at which the bridge service will server healthz, readyz, and status calls.
ASTRIA_BRIDGE_WITHDRAWER_API_ADDR=127.0.0.1:2450

# Set to true to enable prometheus metrics.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Set to true to enable prometheus metrics.
# Set to true to disable prometheus metrics.

Comment on lines 22 to 25
use ethers::types::{
TxHash,
U64,
};
Copy link
Member

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 a bit more clear further down if these types weren't "used" and instead fully specify the type path below. I was confused about why u64 was capitalized further down and it's good to have clarity on what type of txHash we are talking about (it's an EVM tx hash not a cometbft one)

Copy link
Contributor

Choose a reason for hiding this comment

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

moved to the ethereum crate as this logic is eth-specific

/// It also contains a `return_address` field which may or may not be the same as the signer
/// of the packet. The funds will be returned to the `return_address` in the case of a timeout.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
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 make a follow up issue here? seems a general code quality thing

solc-select install 0.8.21
solc-select use 0.8.21
cargo nextest run --archive-file=archive.tar.zst -- --include-ignored
cargo nextest run --package astria-bridge-withdrawer -- --include-ignored
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
cargo nextest run --package astria-bridge-withdrawer -- --include-ignored
cargo nextest run --package astria-bridge-withdrawer -- --include-ignored

.metrics_addr(&cfg.metrics_http_listener_addr)
.service_name(env!("CARGO_PKG_NAME"));
}
metrics_init::register();
Copy link
Contributor

Choose a reason for hiding this comment

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

Please register metrics the same way as all other services

Copy link
Contributor

Choose a reason for hiding this comment

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

Where is this file used?

Copy link
Contributor

Choose a reason for hiding this comment

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

this is used to deploy the ethereum contract and make calls to it - see the readme in this crate

@@ -0,0 +1,20 @@
# anvil account 0
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. We should never leak private keys in plain text.

# Address of cometbft/tendermint to request new block heights.
# 127.0.0.1:26657 is the default socket address at which cometbft
# serves RPCs.
ASTRIA_BRIDGE_WITHDRAWER_COMETBFT_ENDPOINT="http://127.0.0.1:26657"
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's be clearer about this. This reads like it's the withdrawer's endpoint.

Suggested change
ASTRIA_BRIDGE_WITHDRAWER_COMETBFT_ENDPOINT="http://127.0.0.1:26657"
ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_COMETBFT_ENDPOINT="http://127.0.0.1:26657"

mod state;
mod submitter;

pub struct Service {
Copy link
Contributor

Choose a reason for hiding this comment

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

Call this WithdrawerBridge following all our other services.

Copy link
Contributor

Choose a reason for hiding this comment

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

that cause a clippy name repetitions error since this module is already called withdrawer and this isn't a bridge, so i'd prefer not to name it that

Comment on lines 188 to 195
if let Ok((event, meta)) = item {
event_tx
.send((WithdrawalEvent::Ics20(event), meta))
.await
.wrap_err("failed to send ics20 withdrawal event; receiver dropped?")?;
} else if let Err(e) = item {
return Err(e).wrap_err("failed to read from event stream; event stream closed?");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

As above, please simplify this.

info!("batcher shutting down");
break;
}
item = self.event_rx.recv() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This select! statement should break if self.event_rx.recv() == None because that means the channel is closed.

let bytes = hex::decode(s).wrap_err("failed to parse ethereum address as hex")?;
let address: [u8; 20] = bytes
.try_into()
.map_err(|_| eyre!("invalid length for ethereum address, must be 20 bytes"))?;
Copy link
Contributor

Choose a reason for hiding this comment

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

report the actual number of bytes received

/// It also contains a `return_address` field which may or may not be the same as the signer
/// of the packet. The funds will be returned to the `return_address` in the case of a timeout.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Gonna let it pass here because the other actions also have their fields exposed. But yeah, in general I prefer to not leak that stuff. We should address this in a follow, prior to publishing this crate.

}

/// Queries the sequencer for the latest nonce with an exponential backoff
#[instrument(name = "get_latest_nonce", skip_all, fields(%address))]
Copy link
Contributor

Choose a reason for hiding this comment

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

name matches the function name. You can remove that attribute.

@SuperFluffy
Copy link
Contributor

I would like a simple blackbox test here that shows that we can spin up the service, receive an event, and see it come out on the other side.

@noot
Copy link
Contributor

noot commented May 31, 2024

@SuperFluffy comments are addressed, blackbox test will be done in a PR shortly

@noot noot requested a review from SuperFluffy May 31, 2024 21:20
@noot noot enabled auto-merge May 31, 2024 21:20
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.

Re-approving noting this is blocking further development and blackbox tests are actively being built, we have a tight timeline can't delay on the tests.

@joroshiba joroshiba dismissed SuperFluffy’s stale review June 1, 2024 00:03

blackbox tests non-blocking for greenfield work, in progress but this PR is blocking other work

@noot noot added this pull request to the merge queue Jun 1, 2024
Merged via the queue into main with commit afe4901 Jun 1, 2024
@noot noot deleted the itamarreif/bridge-scaffolding branch June 1, 2024 00:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bridging ci issues that are related to ci and github workflows composer pertaining to composer conductor pertaining to the astria-conductor crate sequencer-relayer pertaining to the astria-sequencer-relayer crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bridge-withdrawer: Add event derivation logic for sequencer-native withdrawals

4 participants