Skip to content

Commit

Permalink
Merge pull request #64 from buffrr/sign-verify
Browse files Browse the repository at this point in the history
RPC sign and verify messages with a space
  • Loading branch information
buffrr authored Jan 26, 2025
2 parents 222634c + 19cffe0 commit 2d6620d
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 21 deletions.
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

0 comments on commit 2d6620d

Please sign in to comment.