Skip to content

Commit

Permalink
feat(sequencer): integrate connect oracle and vote extension logic (#…
Browse files Browse the repository at this point in the history
…1236)

## Summary
integrate skip's [connect](https://github.com/skip-mev/connect)
(formerly named `slinky`) oracle service into astria.

at a high level, connect consists of an oracle sidecar program, which
interacts with a validator node to provide price data, and various
cosmos-sdk modules.

since astria isn't cosmos, the relevant cosmos modules (x/marketmap and
x/oracle) were essentially ported into the `connect` module of the
sequencer app, which consists of two components, `market_map` and
`oracle`.

the sequencer app was updated to talk to the sidecar during the
`extend_vote` phase of consensus to gather prices to put into a vote
extension.

the vote extension validation logic, proposal logic, and finalization
logic were also ported from connect.

## Background
we want oracle data to be available to rollups (and maybe on the
sequencer itself too?)

## Changes
* import relevant protos from connect and create native rust types for
them
* update the sequencer genesis state to contain `market_map` and
`oracle` values
* implement the `market_map` component for the sequencer
* update the sequencer grpc service to support the market_map grpc
service, which is required by the oracle sidecar to retrieve the market
map from the chain
* implement the `oracle` component for the sequencer and the query
service for this component
* implement `extend_vote` logic which gets the prices from the sidecar
and turns them into a vote extension
* implement `verify_vote_extension` logic which performs basic
validation on a vote extension during the consensus phase
* implement `prepare_proposal` logic which gathers the vote extensions
from the previous block, prunes any invalid votes, and performs
additional validation to create a valid set of VEs
* implement `process_proposal` logic which validates the set of VEs
proposed, checking signatures and that the voting power is >2/3 amongst
other things
* implement `finalize_block` logic which writes the updated prices to
state based on the committed vote extensions. skip uses stake-weighted
median to calculate the final price, but we don't have stake-weighting
yet, so i just took the median.
* TODO: implement the connect cosmos `Msg` types as sequencer actions
(follow-up)
*
https://github.com/skip-mev/slinky/blob/158cde8a4b774ac4eec5c6d1a2c16de6a8c6abb5/proto/slinky/oracle/v1/tx.proto
*
https://github.com/skip-mev/slinky/blob/158cde8a4b774ac4eec5c6d1a2c16de6a8c6abb5/proto/slinky/marketmap/v1/tx.proto
* TODO: update `SequencerBlockHeader` to contain the extended commit
info + a proof for it (also follow-up)
* TODO: implement the `DeltaCurrencyPairStrategy` - right now only the
`DefaultCurrencyPairStrategy` is implemented. can also do in follow-up

## Testing
TODO: run this on a multi-validator network also

clone connect: https://github.com/skip-mev/connect/tree/main

install go 1.22

build and run connect:

```sh
make build
go run scripts/genesis.go --use-coingecko=true --temp-file=markets.json
./build/connect --market-config-path markets.json --port 8081
```

checkout `noot/slinky` branch of astria

run sequencer app and `ASTRIA_SEQUENCER_NO_ORACLE=false` in `.env`:

```sh
rm -rf /tmp/astria_db
rm -rf ~/.cometbft
just run
just run-cometbft
```

should see a sequencer log like:

```sh
astria_sequencer::sequencer: oracle sidecar is reachable
```

should see a connect log like:


```sh
{"level":"info","ts":"2024-07-02T14:33:46.318-0400","caller":"marketmap/fetcher.go:147","msg":"successfully fetched market map data from module; checking if market map has changed","pid":727051,"process":"oracle","fetcher":"marketmap_api"}
```

then, when blocks are made, should see logs like the following for each
block:


```
2024-07-05T02:49:21.254163Z DEBUG handle_request:handle_process_proposal: astria_sequencer::service::consensus: proposal processed height=28 time=2024-07-05T02:49:19.143352683Z tx_count=3 proposer=7BE21CDEB6FDCC9299A51F44C6B390EA990E88CD hash=7rmdhOsaW2a0NCZUwSE5yqt2AVR3cOPgGGb4Bb0kpRM= next_validators_hash=F6N7YDQZKfXQld95iV0AmQKNa8DiAxrDnTAcn323QSU=
2024-07-05T02:49:21.310218Z DEBUG handle_request:handle_extend_vote:App::extend_vote: astria_sequencer::app::vote_extension: got prices from oracle sidecar; transforming prices prices_count=118
2024-07-05T02:49:21.323262Z DEBUG handle_request:handle_extend_vote:App::extend_vote: astria_sequencer::app::vote_extension: transformed price for inclusion in vote extension currency_pair="BTC/USD" id=0 price=5683583007
2024-07-05T02:49:21.326070Z DEBUG handle_request:handle_extend_vote:App::extend_vote: astria_sequencer::app::vote_extension: transformed price for inclusion in vote extension currency_pair="ETH/USD" id=1 price=3055069469
2024-07-05T02:49:21.384266Z DEBUG handle_request:finalize_block:App::finalize_block: astria_sequencer::app::vote_extension: applied price from vote extension currency_pair="BTC/USD" price=5683583007 hash=EEB99D84EB1A5B66B4342654C12139CAAB7601547770E3E01866F805BD24A513 height=28 time=2024-07-05T02:49:19.143352683Z proposer=7BE21CDEB6FDCC9299A51F44C6B390EA990E88CD
2024-07-05T02:49:21.384553Z DEBUG handle_request:finalize_block:App::finalize_block: astria_sequencer::app::vote_extension: applied price from vote extension currency_pair="ETH/USD" price=3055069469 hash=EEB99D84EB1A5B66B4342654C12139CAAB7601547770E3E01866F805BD24A513 height=28 time=2024-07-05T02:49:19.143352683Z proposer=7BE21CDEB6FDCC9299A51F44C6B390EA990E88CD
```

## Breaking Changelist
* the PR adds a new proposer-added transaction at
the start of the block only if vote extensions are enabled. then, there will be 3 special "txs" expected when
before there were only 2. however if vote extensions are disabled, this won't make a difference.
* the genesis was updated with a new optional field `connect`, however
as this field is optional, it is non-breaking with existing networks.
* if the `connect` genesis field is set, the sequencer state will
change, as the genesis state changes and new values are stored in state.
however this does not affect syncing existing networks, as the genesis
of the existing network can be used as-is.
* additionally, vote extension participation is optional - if <=2/3
validators participate, blocks are still finalized, just no new oracle
data will be published.

---------

Co-authored-by: Richard Janis Goldschmidt <github@aberrat.io>
  • Loading branch information
noot and SuperFluffy authored Nov 6, 2024
1 parent 3c6e456 commit 1450ca5
Show file tree
Hide file tree
Showing 84 changed files with 11,269 additions and 471 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion charts/sequencer/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.0
version: 1.0.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
Expand Down
26 changes: 25 additions & 1 deletion charts/sequencer/files/cometbft/config/genesis.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,24 @@
{{- if $index }},{{- end }}
{{ include "sequencer.address" $value }}
{{- end }}
]
],
{{- if not .Values.global.dev }}
{{- else }}
"connect": {
"marketMap": {
"marketMap": {
"markets": {}
},
"params": {
"marketAuthorities": [],
"admin": "{{ .Values.genesis.marketAdminAddress }}"
}
},
"oracle": {
"currencyPairGenesis": [],
"nextId": "0"
}
}
{{- end}}
},
"chain_id": "{{ .Values.genesis.chainId }}",
Expand All @@ -142,7 +157,16 @@
},
"version": {
"app": "0"
},
{{- if not .Values.global.dev }}
"abci": {
"vote_extensions_enable_height": "0"
}
{{- else }}
"abci": {
"vote_extensions_enable_height": "1"
}
{{- end}}
},
"genesis_time": "{{ .Values.genesis.genesisTime }}",
"initial_height": "0",
Expand Down
3 changes: 3 additions & 0 deletions charts/sequencer/templates/configmaps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ data:
OTEL_EXPORTER_OTLP_HEADERS: "{{ .Values.sequencer.otel.otlpHeaders }}"
OTEL_EXPORTER_OTLP_TRACE_HEADERS: "{{ .Values.sequencer.otel.traceHeaders }}"
OTEL_SERVICE_NAME: "{{ tpl .Values.sequencer.otel.serviceName . }}"
ASTRIA_SEQUENCER_ORACLE_GRPC_ADDR: "http://127.0.0.1:{{ .Values.ports.oracleGrpc }}"
ASTRIA_SEQUENCER_ORACLE_CLIENT_TIMEOUT_MILLISECONDS: "{{ .Values.sequencer.oracle.clientTimeout }}"
{{- if not .Values.global.dev }}
{{- else }}
ASTRIA_SEQUENCER_NO_ORACLE: "true"
{{- end }}
---
5 changes: 5 additions & 0 deletions charts/sequencer/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ genesis:
base: "astria"
ibcCompat: "astriacompat"
authoritySudoAddress: ""
marketAdminAddress: ""
allowedFeeAssets: []
# - nria
ibc:
Expand Down Expand Up @@ -108,6 +109,8 @@ sequencer:
mempool:
parked:
maxTxCount: 200
oracle:
clientTimeout: 1000
metrics:
enabled: false
otel:
Expand Down Expand Up @@ -271,9 +274,11 @@ ports:
cometbftRpc: 26657
cometbftMetrics: 26660
sequencerABCI: 26658
# note: the oracle sidecar also uses 8080 by default but can be changed with --port
sequencerGrpc: 8080
relayerRpc: 2450
sequencerMetrics: 9000
oracleGrpc: 8081

# ServiceMonitor configuration
serviceMonitor:
Expand Down
1 change: 1 addition & 0 deletions crates/astria-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ penumbra-ibc = { workspace = true }
penumbra-proto = { workspace = true }
prost = { workspace = true }
rand = { workspace = true }
regex = { workspace = true }
serde = { workspace = true, features = ["derive"], optional = true }
sha2 = { workspace = true }
tendermint = { workspace = true }
Expand Down
74 changes: 74 additions & 0 deletions crates/astria-core/src/connect/abci.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
pub mod v2 {
use bytes::Bytes;
use indexmap::IndexMap;

use crate::{
connect::types::v2::{
CurrencyPairId,
Price,
},
generated::connect::abci::v2 as raw,
};

#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct OracleVoteExtensionError(#[from] OracleVoteExtensionErrorKind);

#[derive(Debug, thiserror::Error)]
#[error("failed to validate connect.abci.v2.OracleVoteExtension")]
enum OracleVoteExtensionErrorKind {
#[error("failed decoding price value in .prices field for key `{id}`")]
DecodePrice {
id: u64,
source: crate::connect::types::v2::DecodePriceError,
},
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OracleVoteExtension {
pub prices: IndexMap<CurrencyPairId, Price>,
}

impl OracleVoteExtension {
/// Converts an on-wire [`raw::OracleVoteExtension`] to a validated domain type
/// [`OracleVoteExtension`].
///
/// # Errors
/// Returns an error if a value in the `.prices` map could not be validated.
pub fn try_from_raw(
raw: raw::OracleVoteExtension,
) -> Result<Self, OracleVoteExtensionError> {
let prices = raw
.prices
.into_iter()
.map(|(id, price)| {
let price = Price::try_from(price).map_err(|source| {
OracleVoteExtensionErrorKind::DecodePrice {
id,
source,
}
})?;
Ok::<_, OracleVoteExtensionErrorKind>((CurrencyPairId::new(id), price))
})
.collect::<Result<_, _>>()?;
Ok(Self {
prices,
})
}

#[must_use]
pub fn into_raw(self) -> raw::OracleVoteExtension {
fn encode_price(input: Price) -> Bytes {
Bytes::copy_from_slice(&input.get().to_be_bytes())
}

raw::OracleVoteExtension {
prices: self
.prices
.into_iter()
.map(|(id, price)| (id.get(), encode_price(price)))
.collect(),
}
}
}
}
Loading

0 comments on commit 1450ca5

Please sign in to comment.