Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RPC sign and verify messages with a space #64

Merged
merged 3 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions node/src/bin/space-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use spaced::{
store::Sha256,
wallets::AddressKind,
};
use spaced::rpc::SignedMessage;
use wallet::bitcoin::secp256k1::schnorr::Signature;
use wallet::export::WalletExport;
use wallet::Listing;
Expand Down Expand Up @@ -193,6 +194,27 @@ enum Commands {
#[arg(long, short)]
fee_rate: Option<u64>,
},
/// Sign a message using the owner address of the specified space
#[command(name = "signmessage")]
SignMessage {
/// The space to use
space: String,
/// The message to sign
message: String,
},
/// Verify a message using the owner address of the specified space
#[command(name = "verifymessage")]
VerifyMessage {
/// The space to verify
space: String,

/// The message to verify
message: String,

/// The signature to verify
#[arg(long)]
signature: String,
},
/// List a space you own for sale
#[command(name = "sell")]
Sell {
Expand Down Expand Up @@ -700,6 +722,25 @@ async fn handle_commands(
.verify_listing(listing).await?;
println!("{}", serde_json::to_string_pretty(&result).expect("result"));
}
Commands::SignMessage { mut space, message } => {
space = normalize_space(&space);
let result = cli.client
.wallet_sign_message(&cli.wallet, &space, protocol::Bytes::new(message.as_bytes().to_vec())).await?;
println!("{}", result.signature);
}
Commands::VerifyMessage { mut space, message, signature } => {
space = normalize_space(&space);
let raw = hex::decode(signature)
.map_err(|_| ClientError::Custom("Invalid signature".to_string()))?;
let signature = Signature::from_slice(raw.as_slice())
.map_err(|_| ClientError::Custom("Invalid signature".to_string()))?;
let result = cli.client.verify_message(SignedMessage {
space,
message: protocol::Bytes::new(message.as_bytes().to_vec()),
signature,
}).await?;
println!("{}", serde_json::to_string_pretty(&result).expect("result"));
}
}

Ok(())
Expand Down
65 changes: 51 additions & 14 deletions node/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,18 @@ use bdk::{
};
use jsonrpsee::{core::async_trait, proc_macros::rpc, server::Server, types::ErrorObjectOwned};
use log::info;
use protocol::{
bitcoin,
bitcoin::{
bip32::Xpriv,
Network::{Regtest, Testnet},
OutPoint,
},
constants::ChainAnchor,
hasher::{BaseHash, KeyHasher, SpaceKey},
prepare::DataSource,
slabel::SLabel,
validate::TxChangeSet,
FullSpaceOut, SpaceOut,
};
use protocol::{bitcoin, bitcoin::{
bip32::Xpriv,
Network::{Regtest, Testnet},
OutPoint,
}, constants::ChainAnchor, hasher::{BaseHash, KeyHasher, SpaceKey}, prepare::DataSource, slabel::SLabel, validate::TxChangeSet, Bytes, FullSpaceOut, SpaceOut};
use serde::{Deserialize, Serialize};
use tokio::{
select,
sync::{broadcast, mpsc, oneshot, RwLock},
task::JoinSet,
};
use protocol::bitcoin::secp256k1;
use wallet::{bdk_wallet as bdk, bdk_wallet::template::Bip86, bitcoin::hashes::Hash, export::WalletExport, Balance, DoubleUtxo, Listing, SpacesWallet, WalletConfig, WalletDescriptors, WalletInfo, WalletOutput};

use crate::{
Expand All @@ -58,6 +50,13 @@ pub struct ServerInfo {
pub tip: ChainAnchor,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedMessage {
pub space: String,
pub message: protocol::Bytes,
pub signature: secp256k1::schnorr::Signature,
}

pub enum ChainStateCommand {
CheckPackage {
txs: Vec<String>,
Expand Down Expand Up @@ -99,6 +98,10 @@ pub enum ChainStateCommand {
listing: Listing,
resp: Responder<anyhow::Result<()>>,
},
VerifyMessage {
msg: SignedMessage,
resp: Responder<anyhow::Result<()>>,
},
}

#[derive(Clone)]
Expand Down Expand Up @@ -153,6 +156,12 @@ pub trait Rpc {
#[method(name = "walletimport")]
async fn wallet_import(&self, wallet: WalletExport) -> Result<(), ErrorObjectOwned>;

#[method(name = "verifymessage")]
async fn verify_message(&self, msg: SignedMessage) -> Result<(), ErrorObjectOwned>;

#[method(name = "walletsignmessage")]
async fn wallet_sign_message(&self, wallet: &str, space: &str, msg: protocol::Bytes) -> Result<SignedMessage, ErrorObjectOwned>;

#[method(name = "walletgetinfo")]
async fn wallet_get_info(&self, name: &str) -> Result<WalletInfo, ErrorObjectOwned>;

Expand Down Expand Up @@ -797,13 +806,28 @@ impl RpcServer for RpcServerImpl {
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn wallet_sign_message(&self, wallet: &str, space: &str, msg: Bytes) -> Result<SignedMessage, ErrorObjectOwned> {
self.wallet(&wallet)
.await?
.send_sign_message(space, msg)
.await
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn verify_listing(&self, listing: Listing) -> Result<(), ErrorObjectOwned> {
self.store
.verify_listing(listing)
.await
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn verify_message(&self, msg: SignedMessage) -> Result<(), ErrorObjectOwned> {
self.store
.verify_message(msg)
.await
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn wallet_list_transactions(
&self,
wallet: &str,
Expand Down Expand Up @@ -1006,6 +1030,11 @@ impl AsyncChainState {
ChainStateCommand::VerifyListing { listing, resp } => {
_ = resp.send(SpacesWallet::verify_listing::<Sha256>(chain_state, &listing).map(|_| ()));
}
ChainStateCommand::VerifyMessage { msg, resp } => {
_ = resp.send(SpacesWallet::verify_message::<Sha256>(
chain_state, &msg.space, msg.message.as_slice(), &msg.signature
).map(|_| ()));
}
}
}

Expand Down Expand Up @@ -1047,6 +1076,14 @@ impl AsyncChainState {
resp_rx.await?
}

pub async fn verify_message(&self, msg: SignedMessage) -> anyhow::Result<()> {
let (resp, resp_rx) = oneshot::channel();
self.sender
.send(ChainStateCommand::VerifyMessage { msg, resp })
.await?;
resp_rx.await?
}

pub async fn get_rollout(&self, target: usize) -> anyhow::Result<Vec<RolloutEntry>> {
let (resp, resp_rx) = oneshot::channel();
self.sender
Expand Down
36 changes: 36 additions & 0 deletions node/src/wallets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use wallet::{address::SpaceAddress, bdk_wallet::{
use crate::{checker::TxChecker, config::ExtendedNetwork, node::BlockSource, rpc::{RpcWalletRequest, RpcWalletTxBuilder, WalletLoadRequest}, source::{
BitcoinBlockSource, BitcoinRpc, BitcoinRpcError, BlockEvent, BlockFetchError, BlockFetcher,
}, std_wait, store::{ChainState, LiveSnapshot, Sha256}};
use crate::rpc::SignedMessage;

const MEMPOOL_CHECK_INTERVAL: Duration = Duration::from_millis(
if cfg!(debug_assertions) { 500 } else { 10_000 }
Expand Down Expand Up @@ -111,6 +112,11 @@ pub enum WalletCommand {
resp: crate::rpc::Responder<anyhow::Result<Balance>>,
},
UnloadWallet,
SignMessage {
space: String,
msg: protocol::Bytes,
resp: crate::rpc::Responder<anyhow::Result<SignedMessage>>,
}
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, ValueEnum)]
Expand Down Expand Up @@ -375,6 +381,20 @@ impl RpcWallet {
WalletCommand::Sell { space, price, resp } => {
_ = resp.send(wallet.sell::<Sha256>(state, &space, Amount::from_sat(price)));
}
WalletCommand::SignMessage { space, msg, resp } => {
match wallet.sign_message::<Sha256>(state, &space, msg.as_slice()) {
Ok(signature) => {
_ = resp.send(Ok(SignedMessage {
space,
message: msg,
signature,
}));
}
Err(err) => {
_ = resp.send(Err(err));
}
}
}
}
Ok(())
}
Expand Down Expand Up @@ -1172,6 +1192,22 @@ impl RpcWallet {
resp_rx.await?
}

pub async fn send_sign_message(
&self,
space: &str,
msg: protocol::Bytes
) -> anyhow::Result<SignedMessage> {
let (resp, resp_rx) = oneshot::channel();
self.sender
.send(WalletCommand::SignMessage {
space: space.to_string(),
msg,
resp,
})
.await?;
resp_rx.await?
}

pub async fn send_list_transactions(
&self,
count: usize,
Expand Down
35 changes: 32 additions & 3 deletions node/tests/integration_tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{path::PathBuf, str::FromStr};
use protocol::{bitcoin::{Amount, FeeRate}, constants::RENEWAL_INTERVAL, script::SpaceScript, Covenant};
use protocol::{bitcoin::{Amount, FeeRate}, constants::RENEWAL_INTERVAL, script::SpaceScript, Bytes, Covenant};
use spaced::{
rpc::{
BidParams, ExecuteParams, OpenParams, RegisterParams, RpcClient, RpcWalletRequest,
Expand Down Expand Up @@ -1025,6 +1025,34 @@ async fn it_should_allow_buy_sell(rig: &TestRig) -> anyhow::Result<()> {
Ok(())
}

async fn it_should_allow_sign_verify_messages(rig: &TestRig) -> anyhow::Result<()> {
rig.wait_until_wallet_synced(BOB).await.expect("synced");

let alice_spaces = rig.spaced.client.wallet_list_spaces(BOB).await.expect("bob spaces");
let space = alice_spaces.owned.first().expect("bob should have at least 1 space");

let space_name = space.spaceout.space.as_ref().unwrap().name.to_string();

let msg = Bytes::new(b"hello world".to_vec());
let signed = rig.spaced.client.wallet_sign_message(BOB, &space_name, msg.clone()).await.expect("sign");

println!("signed\n{}", serde_json::to_string_pretty(&signed).unwrap());
assert_eq!(signed.space, space_name, "bad signer");
assert_eq!(signed.message.as_slice(), msg.as_slice(), "msg content must match");

rig.spaced.client.verify_message(signed.clone()).await.expect("verify");

let mut bad_signer = signed.clone();
bad_signer.space = "@nothanks".to_string();
rig.spaced.client.verify_message(bad_signer).await.expect_err("bad signer");

let mut bad_msg = signed.clone();
bad_msg.message = Bytes::new(b"hello world 2".to_vec());
rig.spaced.client.verify_message(bad_msg).await.expect_err("bad msg");

Ok(())
}

async fn it_should_handle_reorgs(rig: &TestRig) -> anyhow::Result<()> {
rig.wait_until_wallet_synced(ALICE).await.expect("synced");
const NAME: &str = "hello_world";
Expand Down Expand Up @@ -1066,10 +1094,11 @@ async fn run_auction_tests() -> anyhow::Result<()> {
.expect("should not allow register/transfer multiple times");
it_can_batch_txs(&rig).await.expect("bump fee");
it_can_use_reserved_op_codes(&rig).await.expect("should use reserved opcodes");
it_should_allow_buy_sell(&rig).await.expect("should use reserved opcodes");
it_should_allow_buy_sell(&rig).await.expect("should allow buy sell");
it_should_allow_sign_verify_messages(&rig).await.expect("should sign verify");

// keep reorgs last as it can drop some txs from mempool and mess up wallet state
it_should_handle_reorgs(&rig).await.expect("should make wallet");
it_should_handle_reorgs(&rig).await.expect("should handle reorgs wallet");
Ok(())
}

Expand Down
Loading
Loading