Skip to content

Commit

Permalink
Implement auto-cycling of Noble deposit addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
zbuc committed Oct 2, 2024
1 parent 772fc69 commit c78757c
Showing 1 changed file with 232 additions and 93 deletions.
325 changes: 232 additions & 93 deletions crates/bin/pcli/src/command/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use std::{
io::{Read, Write},
path::PathBuf,
str::FromStr,
time::{SystemTime, UNIX_EPOCH},
time::{Duration, SystemTime, UNIX_EPOCH},
};

use anyhow::{ensure, Context, Result};
use decaf377::{Fq, Fr};
use futures::future::{BoxFuture, FutureExt};
use ibc_proto::ibc::core::client::v1::{
query_client::QueryClient as IbcClientQueryClient, QueryClientStateRequest,
};
Expand All @@ -30,7 +31,7 @@ use penumbra_asset::{asset, asset::Metadata, Value, STAKING_TOKEN_ASSET_ID};
use penumbra_dex::{lp::position, swap_claim::SwapClaimPlan};
use penumbra_fee::FeeTier;
use penumbra_governance::{proposal::ProposalToml, proposal_state::State as ProposalState, Vote};
use penumbra_keys::{keys::AddressIndex, Address};
use penumbra_keys::{address::NobleForwardingAddress, keys::AddressIndex, Address, FullViewingKey};
use penumbra_num::Amount;
use penumbra_proto::{
core::component::{
Expand Down Expand Up @@ -278,12 +279,13 @@ pub enum TxCmd {
/// The Noble IBC channel to use for forwarding.
#[clap(long)]
channel: String,
/// The Penumbra address or address index to receive forwarded funds.
/// Optional.
///
/// The Penumbra account index to receive forwarded funds.
///
/// If unset, account 0 will be used.
#[clap(long)]
address_or_index: String,
/// Whether or not to use an ephemeral address.
#[clap(long)]
ephemeral: bool,
account: Option<u32>,
},
/// Broadcast a saved transaction to the network
#[clap(display_order = 1000)]
Expand Down Expand Up @@ -1366,106 +1368,243 @@ impl TxCmd {
TxCmd::RegisterForwardingAccount {
noble_node,
channel,
address_or_index,
ephemeral,
account,
} => {
let index: Result<u32, _> = address_or_index.parse();
let fvk = app.config.full_viewing_key.clone();
let next_sequence: u16 =
get_next_noble_sequence(*account, &fvk, channel, noble_node).await?;

let address = if let Ok(index) = index {
// address index provided
let (address, _dtk) = match ephemeral {
false => fvk.incoming().payment_address(index.into()),
true => fvk.incoming().ephemeral_address(OsRng, index.into()),
};

address
} else {
// address or nothing provided
let address: Address = address_or_index
.parse()
.map_err(|_| anyhow::anyhow!("Provided address is invalid."))?;

address
};

let address = get_forwarding_address_for_sequence(next_sequence, *account, &fvk);
let noble_address = address.noble_forwarding_address(channel);

println!("next one-time use Noble forwarding address for account {} is: {}\n\nplease deposit funds to this address...\n\nawaiting deposit...\n\n", account.unwrap_or_default(), noble_address);

wait_for_noble_deposit(noble_node, &noble_address, &address, channel).await?;
println!(
"registering Noble forwarding account with address {} to forward to Penumbra address {}...",
"💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫💫\n\nregistered Noble forwarding account with address {} to forward to Penumbra address {}...\n\nyour funds should show up in your Penumbra account shortly",
noble_address, address
);
}
}

let mut noble_client = CosmosServiceClient::new(
Channel::from_shared(noble_node.to_string())?
.tls_config(ClientTlsConfig::new())?
.connect()
.await?,
);
Ok(())
}
}

let tx = CosmosTx {
body: Some(CosmosTxBody {
messages: vec![pbjson_types::Any {
type_url: MsgRegisterAccount::type_url(),
value: MsgRegisterAccount {
signer: noble_address.to_string(),
recipient: address.to_string(),
channel: channel.to_string(),
}
.encode_to_vec()
.into(),
}],
memo: "".to_string(),
timeout_height: 0,
extension_options: vec![],
non_critical_extension_options: vec![],
}),
auth_info: Some(CosmosAuthInfo {
signer_infos: vec![CosmosSignerInfo {
public_key: Some(pbjson_types::Any {
type_url: ForwardingPubKey::type_url(),
value: ForwardingPubKey {
key: noble_address.bytes(),
}
.encode_to_vec()
.into(),
}),
mode_info: Some(ModeInfo {
// SIGN_MODE_DIRECT
sum: Some(Sum::Single(Single { mode: 1 })),
}),
sequence: 0,
}],
fee: Some(CosmosFee {
amount: vec![],
gas_limit: 200000u64,
payer: "".to_string(),
granter: "".to_string(),
}),
tip: None,
}),
signatures: vec![vec![]],
};
let r = noble_client
.broadcast_tx(CosmosBroadcastTxRequest {
tx_bytes: tx.encode_to_vec().into(),
// sync
mode: 2,
})
.await?;
async fn get_next_noble_sequence(
account: Option<u32>,
fvk: &FullViewingKey,
channel: &str,
noble_node: &Url,
) -> Result<u16> {
// perform binary search to find the first unused noble sequence number
// search space (sequence number) is 2 bytes wide
let left = 0u16;
let right = 0xffffu16;
let mid = (left + right) / 2u16;

// attempt to register midpoint
_get_next_noble_sequence(left, right, mid, noble_node, channel, fvk, account).await
}

// let r = noble_client
// .register_account(MsgRegisterAccount {
// signer: noble_address,
// recipient: address.to_string(),
// channel: channel.to_string(),
// })
// .await?;
// Helper function to perform recursive binary search
fn _get_next_noble_sequence<'a>(
left: u16,
right: u16,
mid: u16,
noble_node: &'a Url,
channel: &'a str,
fvk: &'a FullViewingKey,
account: Option<u32>,
) -> BoxFuture<'a, Result<u16>> {
async move {
let address = get_forwarding_address_for_sequence(mid, account, fvk);
let noble_address = address.noble_forwarding_address(channel);
let noble_res =
register_noble_forwarding_account(noble_node, &noble_address, &address, channel)
.await?;
match noble_res {
NobleRegistrationResponse::NeedsDeposit => {
if left == mid || right == mid {
// We've iterated as far as we can, the next sequence number
// should be the midpoint.
return Ok(mid);
}

println!("Noble response: {:?}", r);
// This means the midpoint has not been registered yet. Search the left-hand
// side.
_get_next_noble_sequence(
left,
mid,
(left + mid) / 2,
noble_node,
channel,
fvk,
account,
)
.await
}
NobleRegistrationResponse::Success => {
// This means the midpoint had a deposit in it waiting for registration.
// This will "flush" this unregistered address, however the user still wants a new one, so return the midpoint + 1.
Ok(mid + 1)
}
NobleRegistrationResponse::AlreadyRegistered => {
if left == mid || right == mid {
// We've iterated as far as we can, the next sequence number
// after the midpoint should be the next available sequence number.
return Ok(mid + 1);
}

// This means the midpoint has been registered already. Search the right-hand side.
_get_next_noble_sequence(
mid,
right,
(right + mid) / 2,
noble_node,
channel,
fvk,
account,
)
.await
}
}
}
.boxed()
}

Ok(())
fn get_forwarding_address_for_sequence(
sequence: u16,
account: Option<u32>,
fvk: &FullViewingKey,
) -> Address {
// Noble Randomizer: [0xff; 10] followed by LE16(sequence)
let mut randomizer: [u8; 12] = [0xff; 12];
let seq_bytes = sequence.to_le_bytes();
randomizer[10..].copy_from_slice(&seq_bytes);

let index = AddressIndex {
account: account.unwrap_or_default(),
randomizer,
};

let (address, _dtk) = fvk.incoming().payment_address(index.into());

address
}

async fn register_noble_forwarding_account(
noble_node: &Url,
noble_address: &NobleForwardingAddress,
address: &Address,
channel: &str,
) -> Result<NobleRegistrationResponse> {
let mut noble_client = CosmosServiceClient::new(
Channel::from_shared(noble_node.to_string())?
.tls_config(ClientTlsConfig::new())?
.connect()
.await?,
);

let tx = CosmosTx {
body: Some(CosmosTxBody {
messages: vec![pbjson_types::Any {
type_url: MsgRegisterAccount::type_url(),
value: MsgRegisterAccount {
signer: noble_address.to_string(),
recipient: address.to_string(),
channel: channel.to_string(),
}
.encode_to_vec()
.into(),
}],
memo: "".to_string(),
timeout_height: 0,
extension_options: vec![],
non_critical_extension_options: vec![],
}),
auth_info: Some(CosmosAuthInfo {
signer_infos: vec![CosmosSignerInfo {
public_key: Some(pbjson_types::Any {
type_url: ForwardingPubKey::type_url(),
value: ForwardingPubKey {
key: noble_address.bytes(),
}
.encode_to_vec()
.into(),
}),
mode_info: Some(ModeInfo {
// SIGN_MODE_DIRECT
sum: Some(Sum::Single(Single { mode: 1 })),
}),
sequence: 0,
}],
fee: Some(CosmosFee {
amount: vec![],
gas_limit: 200000u64,
payer: "".to_string(),
granter: "".to_string(),
}),
tip: None,
}),
signatures: vec![vec![]],
};
let r = noble_client
.broadcast_tx(CosmosBroadcastTxRequest {
tx_bytes: tx.encode_to_vec().into(),
// sync
mode: 2,
})
.await?
.into_inner();

let code = r
.tx_response
.ok_or_else(|| anyhow::anyhow!("no tx response"))?
.code;

match code {
9 => Ok(NobleRegistrationResponse::NeedsDeposit),
0 => Ok(NobleRegistrationResponse::Success),
19 => Ok(NobleRegistrationResponse::AlreadyRegistered),
_ => Err(anyhow::anyhow!("unknown response from Noble")),
}
}

#[derive(Debug, Clone, Copy)]
enum NobleRegistrationResponse {
NeedsDeposit,
Success,
AlreadyRegistered,
}

async fn wait_for_noble_deposit(
noble_node: &Url,
noble_address: &NobleForwardingAddress,
address: &Address,
channel: &str,
) -> Result<()> {
// Use exponential backoff to attempt to register the noble address
// until it's successful.
let max_interval = Duration::from_secs(8);
let mut current_interval = Duration::from_secs(1);

loop {
let noble_res =
register_noble_forwarding_account(noble_node, &noble_address, &address, channel)
.await?;
match noble_res {
NobleRegistrationResponse::Success => {
return Ok(());
}
NobleRegistrationResponse::AlreadyRegistered => {
return Ok(());
}
NobleRegistrationResponse::NeedsDeposit => {
// Wait for a bit and try again.
tokio::time::sleep(current_interval).await;
current_interval = std::cmp::min(max_interval, current_interval * 2);
}
}
}
}

0 comments on commit c78757c

Please sign in to comment.