Files
serai/processor/src/multisigs/db.rs
Luke Parker 0f0db14f05 Ethereum Integration (#557)
* Clean up Ethereum

* Consistent contract address for deployed contracts

* Flesh out Router a bit

* Add a Deployer for DoS-less deployment

* Implement Router-finding

* Use CREATE2 helper present in ethers

* Move from CREATE2 to CREATE

Bit more streamlined for our use case.

* Document ethereum-serai

* Tidy tests a bit

* Test updateSeraiKey

* Use encodePacked for updateSeraiKey

* Take in the block hash to read state during

* Add a Sandbox contract to the Ethereum integration

* Add retrieval of transfers from Ethereum

* Add inInstruction function to the Router

* Augment our handling of InInstructions events with a check the transfer event also exists

* Have the Deployer error upon failed deployments

* Add --via-ir

* Make get_transaction test-only

We only used it to get transactions to confirm the resolution of Eventualities.
Eventualities need to be modularized. By introducing the dedicated
confirm_completion function, we remove the need for a non-test get_transaction
AND begin this modularization (by no longer explicitly grabbing a transaction
to check with).

* Modularize Eventuality

Almost fully-deprecates the Transaction trait for Completion. Replaces
Transaction ID with Claim.

* Modularize the Scheduler behind a trait

* Add an extremely basic account Scheduler

* Add nonce uses, key rotation to the account scheduler

* Only report the account Scheduler empty after transferring keys

Also ban payments to the branch/change/forward addresses.

* Make fns reliant on state test-only

* Start of an Ethereum integration for the processor

* Add a session to the Router to prevent updateSeraiKey replaying

This would only happen if an old key was rotated to again, which would require
n-of-n collusion (already ridiculous and a valid fault attributable event). It
just clarifies the formal arguments.

* Add a RouterCommand + SignMachine for producing it to coins/ethereum

* Ethereum which compiles

* Have branch/change/forward return an option

Also defines a UtxoNetwork extension trait for MAX_INPUTS.

* Make external_address exclusively a test fn

* Move the "account" scheduler to "smart contract"

* Remove ABI artifact

* Move refund/forward Plan creation into the Processor

We create forward Plans in the scan path, and need to know their exact fees in
the scan path. This requires adding a somewhat wonky shim_forward_plan method
so we can obtain a Plan equivalent to the actual forward Plan for fee reasons,
yet don't expect it to be the actual forward Plan (which may be distinct if
the Plan pulls from the global state, such as with a nonce).

Also properly types a Scheduler addendum such that the SC scheduler isn't
cramming the nonce to use into the N::Output type.

* Flesh out the Ethereum integration more

* Two commits ago, into the **Scheduler, not Processor

* Remove misc TODOs in SC Scheduler

* Add constructor to RouterCommandMachine

* RouterCommand read, pairing with the prior added write

* Further add serialization methods

* Have the Router's key included with the InInstruction

This does not use the key at the time of the event. This uses the key at the
end of the block for the event. Its much simpler than getting the full event
streams for each, checking when they interlace.

This does not read the state. Every block, this makes a request for every
single key update and simply chooses the last one. This allows pruning state,
only keeping the event tree. Ideally, we'd also introduce a cache to reduce the
cost of the filter (small in events yielded, long in blocks searched).

Since Serai doesn't have any forwarding TXs, nor Branches, nor change, all of
our Plans should solely have payments out, and there's no expectation of a Plan
being made under one key broken by it being received by another key.

* Add read/write to InInstruction

* Abstract the ABI for Call/OutInstruction in ethereum-serai

* Fill out signable_transaction for Ethereum

* Move ethereum-serai to alloy

Resolves #331.

* Use the opaque sol macro instead of generated files

* Move the processor over to the now-alloy-based ethereum-serai

* Use the ecrecover provided by alloy

* Have the SC use nonce for rotation, not session (an independent nonce which wasn't synchronized)

* Always use the latest keys for SC scheduled plans

* get_eventuality_completions for Ethereum

* Finish fleshing out the processor Ethereum integration as needed for serai-processor tests

This doesn't not support any actual deployments, not even the ones simulated by
serai-processor-docker-tests.

* Add alloy-simple-request-transport to the GH workflows

* cargo update

* Clarify a few comments and make one check more robust

* Use a string for 27.0 in .github

* Remove optional from no-longer-optional dependencies in processor

* Add alloy to git deny exception

* Fix no longer optional specification in processor's binaries feature

* Use a version of foundry from 2024

* Correct fetching Bitcoin TXs in the processor docker tests

* Update rustls to resolve RUSTSEC warnings

* Use the monthly nightly foundry, not the deleted daily nightly
2024-04-21 06:02:12 -04:00

261 lines
7.4 KiB
Rust

use std::io;
use ciphersuite::Ciphersuite;
pub use serai_db::*;
use scale::{Encode, Decode};
use serai_client::{primitives::Balance, in_instructions::primitives::InInstructionWithBalance};
use crate::{
Get, Plan,
networks::{Output, Transaction, Network},
};
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum PlanFromScanning<N: Network> {
Refund(N::Output, N::Address),
Forward(N::Output),
}
impl<N: Network> PlanFromScanning<N> {
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let mut kind = [0xff];
reader.read_exact(&mut kind)?;
match kind[0] {
0 => {
let output = N::Output::read(reader)?;
let mut address_vec_len = [0; 4];
reader.read_exact(&mut address_vec_len)?;
let mut address_vec =
vec![0; usize::try_from(u32::from_le_bytes(address_vec_len)).unwrap()];
reader.read_exact(&mut address_vec)?;
let address =
N::Address::try_from(address_vec).map_err(|_| "invalid address saved to disk").unwrap();
Ok(PlanFromScanning::Refund(output, address))
}
1 => {
let output = N::Output::read(reader)?;
Ok(PlanFromScanning::Forward(output))
}
_ => panic!("reading unrecognized PlanFromScanning"),
}
}
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
match self {
PlanFromScanning::Refund(output, address) => {
writer.write_all(&[0])?;
output.write(writer)?;
let address_vec: Vec<u8> =
address.clone().try_into().map_err(|_| "invalid address being refunded to").unwrap();
writer.write_all(&u32::try_from(address_vec.len()).unwrap().to_le_bytes())?;
writer.write_all(&address_vec)
}
PlanFromScanning::Forward(output) => {
writer.write_all(&[1])?;
output.write(writer)
}
}
}
}
create_db!(
MultisigsDb {
NextBatchDb: () -> u32,
PlanDb: (id: &[u8]) -> Vec<u8>,
PlansFromScanningDb: (block_number: u64) -> Vec<u8>,
OperatingCostsDb: () -> u64,
ResolvedDb: (tx: &[u8]) -> [u8; 32],
SigningDb: (key: &[u8]) -> Vec<u8>,
ForwardedOutputDb: (balance: Balance) -> Vec<u8>,
DelayedOutputDb: () -> Vec<u8>
}
);
impl PlanDb {
pub fn save_active_plan<N: Network>(
txn: &mut impl DbTxn,
key: &[u8],
block_number: usize,
plan: &Plan<N>,
operating_costs_at_time: u64,
) {
let id = plan.id();
{
let mut signing = SigningDb::get(txn, key).unwrap_or_default();
// If we've already noted we're signing this, return
assert_eq!(signing.len() % 32, 0);
for i in 0 .. (signing.len() / 32) {
if signing[(i * 32) .. ((i + 1) * 32)] == id {
return;
}
}
signing.extend(&id);
SigningDb::set(txn, key, &signing);
}
{
let mut buf = block_number.to_le_bytes().to_vec();
plan.write(&mut buf).unwrap();
buf.extend(&operating_costs_at_time.to_le_bytes());
Self::set(txn, &id, &buf);
}
}
pub fn active_plans<N: Network>(getter: &impl Get, key: &[u8]) -> Vec<(u64, Plan<N>, u64)> {
let signing = SigningDb::get(getter, key).unwrap_or_default();
let mut res = vec![];
assert_eq!(signing.len() % 32, 0);
for i in 0 .. (signing.len() / 32) {
let id = &signing[(i * 32) .. ((i + 1) * 32)];
let buf = Self::get(getter, id).unwrap();
let block_number = u64::from_le_bytes(buf[.. 8].try_into().unwrap());
let plan = Plan::<N>::read::<&[u8]>(&mut &buf[8 ..]).unwrap();
assert_eq!(id, &plan.id());
let operating_costs = u64::from_le_bytes(buf[(buf.len() - 8) ..].try_into().unwrap());
res.push((block_number, plan, operating_costs));
}
res
}
pub fn plan_by_key_with_self_change<N: Network>(
getter: &impl Get,
key: <N::Curve as Ciphersuite>::G,
id: [u8; 32],
) -> bool {
let plan = Plan::<N>::read::<&[u8]>(&mut &Self::get(getter, &id).unwrap()[8 ..]).unwrap();
assert_eq!(plan.id(), id);
if let Some(change) = N::change_address(plan.key) {
(key == plan.key) && (Some(change) == plan.change)
} else {
false
}
}
}
impl OperatingCostsDb {
pub fn take_operating_costs(txn: &mut impl DbTxn) -> u64 {
let existing = Self::get(txn).unwrap_or_default();
txn.del(Self::key());
existing
}
pub fn set_operating_costs(txn: &mut impl DbTxn, amount: u64) {
if amount != 0 {
Self::set(txn, &amount);
}
}
}
impl ResolvedDb {
pub fn resolve_plan<N: Network>(
txn: &mut impl DbTxn,
key: &[u8],
plan: [u8; 32],
resolution: &<N::Transaction as Transaction<N>>::Id,
) {
let mut signing = SigningDb::get(txn, key).unwrap_or_default();
assert_eq!(signing.len() % 32, 0);
let mut found = false;
for i in 0 .. (signing.len() / 32) {
let start = i * 32;
let end = i + 32;
if signing[start .. end] == plan {
found = true;
signing = [&signing[.. start], &signing[end ..]].concat();
break;
}
}
if !found {
log::warn!("told to finish signing {} yet wasn't actively signing it", hex::encode(plan));
}
SigningDb::set(txn, key, &signing);
Self::set(txn, resolution.as_ref(), &plan);
}
}
impl PlansFromScanningDb {
pub fn set_plans_from_scanning<N: Network>(
txn: &mut impl DbTxn,
block_number: usize,
plans: Vec<PlanFromScanning<N>>,
) {
let mut buf = vec![];
for plan in plans {
plan.write(&mut buf).unwrap();
}
Self::set(txn, block_number.try_into().unwrap(), &buf);
}
pub fn take_plans_from_scanning<N: Network>(
txn: &mut impl DbTxn,
block_number: usize,
) -> Option<Vec<PlanFromScanning<N>>> {
let block_number = u64::try_from(block_number).unwrap();
let res = Self::get(txn, block_number).map(|plans| {
let mut plans_ref = plans.as_slice();
let mut res = vec![];
while !plans_ref.is_empty() {
res.push(PlanFromScanning::<N>::read(&mut plans_ref).unwrap());
}
res
});
if res.is_some() {
txn.del(Self::key(block_number));
}
res
}
}
impl ForwardedOutputDb {
pub fn save_forwarded_output(txn: &mut impl DbTxn, instruction: &InInstructionWithBalance) {
let mut existing = Self::get(txn, instruction.balance).unwrap_or_default();
existing.extend(instruction.encode());
Self::set(txn, instruction.balance, &existing);
}
pub fn take_forwarded_output(
txn: &mut impl DbTxn,
balance: Balance,
) -> Option<InInstructionWithBalance> {
let outputs = Self::get(txn, balance)?;
let mut outputs_ref = outputs.as_slice();
let res = InInstructionWithBalance::decode(&mut outputs_ref).unwrap();
assert!(outputs_ref.len() < outputs.len());
if outputs_ref.is_empty() {
txn.del(&Self::key(balance));
} else {
Self::set(txn, balance, &outputs);
}
Some(res)
}
}
impl DelayedOutputDb {
pub fn save_delayed_output(txn: &mut impl DbTxn, instruction: &InInstructionWithBalance) {
let mut existing = Self::get(txn).unwrap_or_default();
existing.extend(instruction.encode());
Self::set(txn, &existing);
}
pub fn take_delayed_outputs(txn: &mut impl DbTxn) -> Vec<InInstructionWithBalance> {
let Some(outputs) = Self::get(txn) else { return vec![] };
txn.del(Self::key());
let mut outputs_ref = outputs.as_slice();
let mut res = vec![];
while !outputs_ref.is_empty() {
res.push(InInstructionWithBalance::decode(&mut outputs_ref).unwrap());
}
res
}
}