Skip to content

Commit

Permalink
feat: Noble forwarding address registration in pcli (#4865)
Browse files Browse the repository at this point in the history
## Describe your changes

This adds support for registering Noble forwarding addresses, for
example:

`pcli tx register-forwarding-account --noble-node
http://noble-testnet-grpc.polkachu.com:21590 --channel channel-216
--address-or-index 2`

Note that your address will need to be funded prior to being registered;
the [faucet](https://faucet.circle.com/) can be used along with the
`pcli view noble-address ADDRESS_OR_INDEX --channel channel-216` command
to get test funds.

## Issue ticket number and link

Closes #4857

## Checklist before requesting a review

- [ ] If this code contains consensus-breaking changes, I have added the
"consensus-breaking" label. Otherwise, I declare my belief that there
are not consensus-breaking changes, for the following reason:

  > pcli only
  • Loading branch information
zbuc authored Sep 30, 2024
1 parent 555307b commit 772fc69
Show file tree
Hide file tree
Showing 24 changed files with 6,898 additions and 0 deletions.
132 changes: 132 additions & 0 deletions crates/bin/pcli/src/command/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ use penumbra_proto::{
ValidatorPenaltyRequest,
},
},
cosmos::tx::v1beta1::{
mode_info::{Single, Sum},
service_client::ServiceClient as CosmosServiceClient,
AuthInfo as CosmosAuthInfo, BroadcastTxRequest as CosmosBroadcastTxRequest,
Fee as CosmosFee, ModeInfo, SignerInfo as CosmosSignerInfo, Tx as CosmosTx,
TxBody as CosmosTxBody,
},
noble::forwarding::v1::{ForwardingPubKey, MsgRegisterAccount},
view::v1::GasPricesRequest,
Message, Name as _,
};
use penumbra_shielded_pool::Ics20Withdrawal;
use penumbra_stake::rate::RateData;
Expand All @@ -60,6 +69,8 @@ use penumbra_transaction::{gas::swap_claim_gas_cost, Transaction};
use penumbra_view::{SpendableNoteRecord, ViewClient};
use penumbra_wallet::plan::{self, Planner};
use proposal::ProposalCmd;
use tonic::transport::{Channel, ClientTlsConfig};
use url::Url;

use crate::command::tx::auction::AuctionCmd;
use crate::App;
Expand Down Expand Up @@ -258,6 +269,22 @@ pub enum TxCmd {
#[clap(long)]
use_compat_address: bool,
},
#[clap(display_order = 970)]
/// Register a Noble forwarding account.
RegisterForwardingAccount {
/// The Noble node to submit the registration transaction to.
#[clap(long)]
noble_node: Url,
/// The Noble IBC channel to use for forwarding.
#[clap(long)]
channel: String,
/// The Penumbra address or address index to receive forwarded funds.
#[clap(long)]
address_or_index: String,
/// Whether or not to use an ephemeral address.
#[clap(long)]
ephemeral: bool,
},
/// Broadcast a saved transaction to the network
#[clap(display_order = 1000)]
Broadcast {
Expand Down Expand Up @@ -319,13 +346,16 @@ impl TxCmd {
TxCmd::Withdraw { .. } => false,
TxCmd::Auction(_) => false,
TxCmd::Broadcast { .. } => false,
TxCmd::RegisterForwardingAccount { .. } => false,
}
}

pub async fn exec(&self, app: &mut App) -> Result<()> {
// TODO: use a command line flag to determine the fee token,
// and pull the appropriate GasPrices out of this rpc response,
// the rest should follow
// TODO: fetching this here means that no tx commands
// can be run in offline mode, which is a bit annoying
let gas_prices = app
.view
.as_mut()
Expand Down Expand Up @@ -1333,7 +1363,109 @@ impl TxCmd {
let transaction: Transaction = serde_json::from_slice(&fs::read(transaction)?)?;
app.submit_transaction(transaction).await?;
}
TxCmd::RegisterForwardingAccount {
noble_node,
channel,
address_or_index,
ephemeral,
} => {
let index: Result<u32, _> = address_or_index.parse();
let fvk = app.config.full_viewing_key.clone();

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 noble_address = address.noble_forwarding_address(channel);

println!(
"registering Noble forwarding account with address {} to forward to Penumbra address {}...",
noble_address, address
);

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?;

// let r = noble_client
// .register_account(MsgRegisterAccount {
// signer: noble_address,
// recipient: address.to_string(),
// channel: channel.to_string(),
// })
// .await?;

println!("Noble response: {:?}", r);
}
}

Ok(())
}
}
8 changes: 8 additions & 0 deletions crates/bin/pcli/src/command/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use anyhow::Result;

use address::AddressCmd;
use balance::BalanceCmd;
use noble_address::NobleAddressCmd;
use staked::StakedCmd;
use transaction_hashes::TransactionHashesCmd;
use tx::TxCmd;
Expand All @@ -14,6 +15,7 @@ use self::auction::AuctionCmd;
mod address;
mod auction;
mod balance;
mod noble_address;
mod staked;
mod wallet_id;

Expand All @@ -28,6 +30,8 @@ pub enum ViewCmd {
WalletId(WalletIdCmd),
/// View one of your addresses, either by numerical index, or a random ephemeral one.
Address(AddressCmd),
/// View the Noble forwarding address associated with one of your addresses, either by numerical index, or a random ephemeral one.
NobleAddress(NobleAddressCmd),
/// View your account balances.
Balance(BalanceCmd),
/// View your staked delegation tokens.
Expand All @@ -52,6 +56,7 @@ impl ViewCmd {
ViewCmd::Auction(auction_cmd) => auction_cmd.offline(),
ViewCmd::WalletId(wallet_id_cmd) => wallet_id_cmd.offline(),
ViewCmd::Address(address_cmd) => address_cmd.offline(),
ViewCmd::NobleAddress(address_cmd) => address_cmd.offline(),
ViewCmd::Balance(balance_cmd) => balance_cmd.offline(),
ViewCmd::Staked(staked_cmd) => staked_cmd.offline(),
ViewCmd::Reset(_) => true,
Expand Down Expand Up @@ -91,6 +96,9 @@ impl ViewCmd {
ViewCmd::Address(address_cmd) => {
address_cmd.exec(&full_viewing_key)?;
}
ViewCmd::NobleAddress(noble_address_cmd) => {
noble_address_cmd.exec(&full_viewing_key)?;
}
ViewCmd::Balance(balance_cmd) => {
let view_client = app.view();
balance_cmd.exec(view_client).await?;
Expand Down
52 changes: 52 additions & 0 deletions crates/bin/pcli/src/command/view/noble_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use anyhow::Result;
use rand_core::OsRng;

use penumbra_keys::{Address, FullViewingKey};

#[derive(Debug, clap::Parser)]
pub struct NobleAddressCmd {
/// The address to provide information about
#[clap(default_value = "0")]
address_or_index: String,
/// Generate an ephemeral address instead of an indexed one.
#[clap(short, long)]
ephemeral: bool,
/// The Noble IBC channel to use for forwarding.
#[clap(long)]
channel: String,
}

impl NobleAddressCmd {
/// Determine if this command requires a network sync before it executes.
pub fn offline(&self) -> bool {
true
}

pub fn exec(&self, fvk: &FullViewingKey) -> Result<()> {
let index: Result<u32, _> = self.address_or_index.parse();

let address = if let Ok(index) = index {
// address index provided
let (address, _dtk) = match self.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 = self
.address_or_index
.parse()
.map_err(|_| anyhow::anyhow!("Provided address is invalid."))?;

address
};

let noble_address = address.noble_forwarding_address(&self.channel);

println!("{}", noble_address);

Ok(())
}
}
45 changes: 45 additions & 0 deletions crates/core/keys/src/address.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! [Payment address][Address] facilities.

use std::{
fmt::Display,
io::{Cursor, Read, Write},
sync::OnceLock,
};
Expand All @@ -12,6 +13,7 @@ use f4jumble::{f4jumble, f4jumble_inv};
use penumbra_proto::{penumbra::core::keys::v1 as pb, serializers::bech32str, DomainType};
use rand::{CryptoRng, Rng};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

mod r1cs;
pub use r1cs::AddressVar;
Expand Down Expand Up @@ -214,6 +216,49 @@ impl Address {
bech32str::Bech32,
)
}

/// Generate a Noble forwarding address.
pub fn noble_forwarding_address(&self, channel: &str) -> NobleForwardingAddress {
NobleForwardingAddress {
channel: channel.to_string(),
recipient: format!("{}", self),
}
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NobleForwardingAddress {
pub channel: String,
pub recipient: String,
}

impl NobleForwardingAddress {
pub fn bytes(&self) -> Vec<u8> {
// Based on https://github.com/noble-assets/forwarding/blob/main/x/forwarding/types/account.go#L17
let channel = self.channel.clone();
let recipient = self.recipient.clone();
let bz = format!("{channel}{recipient}").as_bytes().to_owned();
let th = Sha256::digest("forwarding".as_bytes());
let mut hasher = Sha256::new();
hasher.update(th);
hasher.update(bz);

// This constructs the account bytes for the Noble forwarding address
// Only use bytes 12 and on:
hasher.finalize()[12..].to_vec()
}
}

impl Display for NobleForwardingAddress {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let addr_bytes = &self.bytes();

write!(
f,
"{}",
bech32str::encode(&addr_bytes, "noble", bech32str::Bech32)
)
}
}

impl DomainType for Address {
Expand Down
Loading

0 comments on commit 772fc69

Please sign in to comment.