Skip to content

Commit 2d6620d

Browse files
authored
Merge pull request #64 from buffrr/sign-verify
RPC sign and verify messages with a space
2 parents 222634c + 19cffe0 commit 2d6620d

File tree

5 files changed

+255
-21
lines changed

5 files changed

+255
-21
lines changed

node/src/bin/space-cli.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use spaced::{
2222
store::Sha256,
2323
wallets::AddressKind,
2424
};
25+
use spaced::rpc::SignedMessage;
2526
use wallet::bitcoin::secp256k1::schnorr::Signature;
2627
use wallet::export::WalletExport;
2728
use wallet::Listing;
@@ -193,6 +194,27 @@ enum Commands {
193194
#[arg(long, short)]
194195
fee_rate: Option<u64>,
195196
},
197+
/// Sign a message using the owner address of the specified space
198+
#[command(name = "signmessage")]
199+
SignMessage {
200+
/// The space to use
201+
space: String,
202+
/// The message to sign
203+
message: String,
204+
},
205+
/// Verify a message using the owner address of the specified space
206+
#[command(name = "verifymessage")]
207+
VerifyMessage {
208+
/// The space to verify
209+
space: String,
210+
211+
/// The message to verify
212+
message: String,
213+
214+
/// The signature to verify
215+
#[arg(long)]
216+
signature: String,
217+
},
196218
/// List a space you own for sale
197219
#[command(name = "sell")]
198220
Sell {
@@ -700,6 +722,25 @@ async fn handle_commands(
700722
.verify_listing(listing).await?;
701723
println!("{}", serde_json::to_string_pretty(&result).expect("result"));
702724
}
725+
Commands::SignMessage { mut space, message } => {
726+
space = normalize_space(&space);
727+
let result = cli.client
728+
.wallet_sign_message(&cli.wallet, &space, protocol::Bytes::new(message.as_bytes().to_vec())).await?;
729+
println!("{}", result.signature);
730+
}
731+
Commands::VerifyMessage { mut space, message, signature } => {
732+
space = normalize_space(&space);
733+
let raw = hex::decode(signature)
734+
.map_err(|_| ClientError::Custom("Invalid signature".to_string()))?;
735+
let signature = Signature::from_slice(raw.as_slice())
736+
.map_err(|_| ClientError::Custom("Invalid signature".to_string()))?;
737+
let result = cli.client.verify_message(SignedMessage {
738+
space,
739+
message: protocol::Bytes::new(message.as_bytes().to_vec()),
740+
signature,
741+
}).await?;
742+
println!("{}", serde_json::to_string_pretty(&result).expect("result"));
743+
}
703744
}
704745

705746
Ok(())

node/src/rpc.rs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,18 @@ use bdk::{
1515
};
1616
use jsonrpsee::{core::async_trait, proc_macros::rpc, server::Server, types::ErrorObjectOwned};
1717
use log::info;
18-
use protocol::{
19-
bitcoin,
20-
bitcoin::{
21-
bip32::Xpriv,
22-
Network::{Regtest, Testnet},
23-
OutPoint,
24-
},
25-
constants::ChainAnchor,
26-
hasher::{BaseHash, KeyHasher, SpaceKey},
27-
prepare::DataSource,
28-
slabel::SLabel,
29-
validate::TxChangeSet,
30-
FullSpaceOut, SpaceOut,
31-
};
18+
use protocol::{bitcoin, bitcoin::{
19+
bip32::Xpriv,
20+
Network::{Regtest, Testnet},
21+
OutPoint,
22+
}, constants::ChainAnchor, hasher::{BaseHash, KeyHasher, SpaceKey}, prepare::DataSource, slabel::SLabel, validate::TxChangeSet, Bytes, FullSpaceOut, SpaceOut};
3223
use serde::{Deserialize, Serialize};
3324
use tokio::{
3425
select,
3526
sync::{broadcast, mpsc, oneshot, RwLock},
3627
task::JoinSet,
3728
};
29+
use protocol::bitcoin::secp256k1;
3830
use wallet::{bdk_wallet as bdk, bdk_wallet::template::Bip86, bitcoin::hashes::Hash, export::WalletExport, Balance, DoubleUtxo, Listing, SpacesWallet, WalletConfig, WalletDescriptors, WalletInfo, WalletOutput};
3931

4032
use crate::{
@@ -58,6 +50,13 @@ pub struct ServerInfo {
5850
pub tip: ChainAnchor,
5951
}
6052

53+
#[derive(Debug, Clone, Serialize, Deserialize)]
54+
pub struct SignedMessage {
55+
pub space: String,
56+
pub message: protocol::Bytes,
57+
pub signature: secp256k1::schnorr::Signature,
58+
}
59+
6160
pub enum ChainStateCommand {
6261
CheckPackage {
6362
txs: Vec<String>,
@@ -99,6 +98,10 @@ pub enum ChainStateCommand {
9998
listing: Listing,
10099
resp: Responder<anyhow::Result<()>>,
101100
},
101+
VerifyMessage {
102+
msg: SignedMessage,
103+
resp: Responder<anyhow::Result<()>>,
104+
},
102105
}
103106

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

159+
#[method(name = "verifymessage")]
160+
async fn verify_message(&self, msg: SignedMessage) -> Result<(), ErrorObjectOwned>;
161+
162+
#[method(name = "walletsignmessage")]
163+
async fn wallet_sign_message(&self, wallet: &str, space: &str, msg: protocol::Bytes) -> Result<SignedMessage, ErrorObjectOwned>;
164+
156165
#[method(name = "walletgetinfo")]
157166
async fn wallet_get_info(&self, name: &str) -> Result<WalletInfo, ErrorObjectOwned>;
158167

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

809+
async fn wallet_sign_message(&self, wallet: &str, space: &str, msg: Bytes) -> Result<SignedMessage, ErrorObjectOwned> {
810+
self.wallet(&wallet)
811+
.await?
812+
.send_sign_message(space, msg)
813+
.await
814+
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
815+
}
816+
800817
async fn verify_listing(&self, listing: Listing) -> Result<(), ErrorObjectOwned> {
801818
self.store
802819
.verify_listing(listing)
803820
.await
804821
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
805822
}
806823

824+
async fn verify_message(&self, msg: SignedMessage) -> Result<(), ErrorObjectOwned> {
825+
self.store
826+
.verify_message(msg)
827+
.await
828+
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
829+
}
830+
807831
async fn wallet_list_transactions(
808832
&self,
809833
wallet: &str,
@@ -1006,6 +1030,11 @@ impl AsyncChainState {
10061030
ChainStateCommand::VerifyListing { listing, resp } => {
10071031
_ = resp.send(SpacesWallet::verify_listing::<Sha256>(chain_state, &listing).map(|_| ()));
10081032
}
1033+
ChainStateCommand::VerifyMessage { msg, resp } => {
1034+
_ = resp.send(SpacesWallet::verify_message::<Sha256>(
1035+
chain_state, &msg.space, msg.message.as_slice(), &msg.signature
1036+
).map(|_| ()));
1037+
}
10091038
}
10101039
}
10111040

@@ -1047,6 +1076,14 @@ impl AsyncChainState {
10471076
resp_rx.await?
10481077
}
10491078

1079+
pub async fn verify_message(&self, msg: SignedMessage) -> anyhow::Result<()> {
1080+
let (resp, resp_rx) = oneshot::channel();
1081+
self.sender
1082+
.send(ChainStateCommand::VerifyMessage { msg, resp })
1083+
.await?;
1084+
resp_rx.await?
1085+
}
1086+
10501087
pub async fn get_rollout(&self, target: usize) -> anyhow::Result<Vec<RolloutEntry>> {
10511088
let (resp, resp_rx) = oneshot::channel();
10521089
self.sender

node/src/wallets.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use wallet::{address::SpaceAddress, bdk_wallet::{
2222
use crate::{checker::TxChecker, config::ExtendedNetwork, node::BlockSource, rpc::{RpcWalletRequest, RpcWalletTxBuilder, WalletLoadRequest}, source::{
2323
BitcoinBlockSource, BitcoinRpc, BitcoinRpcError, BlockEvent, BlockFetchError, BlockFetcher,
2424
}, std_wait, store::{ChainState, LiveSnapshot, Sha256}};
25+
use crate::rpc::SignedMessage;
2526

2627
const MEMPOOL_CHECK_INTERVAL: Duration = Duration::from_millis(
2728
if cfg!(debug_assertions) { 500 } else { 10_000 }
@@ -111,6 +112,11 @@ pub enum WalletCommand {
111112
resp: crate::rpc::Responder<anyhow::Result<Balance>>,
112113
},
113114
UnloadWallet,
115+
SignMessage {
116+
space: String,
117+
msg: protocol::Bytes,
118+
resp: crate::rpc::Responder<anyhow::Result<SignedMessage>>,
119+
}
114120
}
115121

116122
#[derive(Debug, Clone, Copy, Serialize, Deserialize, ValueEnum)]
@@ -375,6 +381,20 @@ impl RpcWallet {
375381
WalletCommand::Sell { space, price, resp } => {
376382
_ = resp.send(wallet.sell::<Sha256>(state, &space, Amount::from_sat(price)));
377383
}
384+
WalletCommand::SignMessage { space, msg, resp } => {
385+
match wallet.sign_message::<Sha256>(state, &space, msg.as_slice()) {
386+
Ok(signature) => {
387+
_ = resp.send(Ok(SignedMessage {
388+
space,
389+
message: msg,
390+
signature,
391+
}));
392+
}
393+
Err(err) => {
394+
_ = resp.send(Err(err));
395+
}
396+
}
397+
}
378398
}
379399
Ok(())
380400
}
@@ -1172,6 +1192,22 @@ impl RpcWallet {
11721192
resp_rx.await?
11731193
}
11741194

1195+
pub async fn send_sign_message(
1196+
&self,
1197+
space: &str,
1198+
msg: protocol::Bytes
1199+
) -> anyhow::Result<SignedMessage> {
1200+
let (resp, resp_rx) = oneshot::channel();
1201+
self.sender
1202+
.send(WalletCommand::SignMessage {
1203+
space: space.to_string(),
1204+
msg,
1205+
resp,
1206+
})
1207+
.await?;
1208+
resp_rx.await?
1209+
}
1210+
11751211
pub async fn send_list_transactions(
11761212
&self,
11771213
count: usize,

node/tests/integration_tests.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{path::PathBuf, str::FromStr};
2-
use protocol::{bitcoin::{Amount, FeeRate}, constants::RENEWAL_INTERVAL, script::SpaceScript, Covenant};
2+
use protocol::{bitcoin::{Amount, FeeRate}, constants::RENEWAL_INTERVAL, script::SpaceScript, Bytes, Covenant};
33
use spaced::{
44
rpc::{
55
BidParams, ExecuteParams, OpenParams, RegisterParams, RpcClient, RpcWalletRequest,
@@ -1025,6 +1025,34 @@ async fn it_should_allow_buy_sell(rig: &TestRig) -> anyhow::Result<()> {
10251025
Ok(())
10261026
}
10271027

1028+
async fn it_should_allow_sign_verify_messages(rig: &TestRig) -> anyhow::Result<()> {
1029+
rig.wait_until_wallet_synced(BOB).await.expect("synced");
1030+
1031+
let alice_spaces = rig.spaced.client.wallet_list_spaces(BOB).await.expect("bob spaces");
1032+
let space = alice_spaces.owned.first().expect("bob should have at least 1 space");
1033+
1034+
let space_name = space.spaceout.space.as_ref().unwrap().name.to_string();
1035+
1036+
let msg = Bytes::new(b"hello world".to_vec());
1037+
let signed = rig.spaced.client.wallet_sign_message(BOB, &space_name, msg.clone()).await.expect("sign");
1038+
1039+
println!("signed\n{}", serde_json::to_string_pretty(&signed).unwrap());
1040+
assert_eq!(signed.space, space_name, "bad signer");
1041+
assert_eq!(signed.message.as_slice(), msg.as_slice(), "msg content must match");
1042+
1043+
rig.spaced.client.verify_message(signed.clone()).await.expect("verify");
1044+
1045+
let mut bad_signer = signed.clone();
1046+
bad_signer.space = "@nothanks".to_string();
1047+
rig.spaced.client.verify_message(bad_signer).await.expect_err("bad signer");
1048+
1049+
let mut bad_msg = signed.clone();
1050+
bad_msg.message = Bytes::new(b"hello world 2".to_vec());
1051+
rig.spaced.client.verify_message(bad_msg).await.expect_err("bad msg");
1052+
1053+
Ok(())
1054+
}
1055+
10281056
async fn it_should_handle_reorgs(rig: &TestRig) -> anyhow::Result<()> {
10291057
rig.wait_until_wallet_synced(ALICE).await.expect("synced");
10301058
const NAME: &str = "hello_world";
@@ -1066,10 +1094,11 @@ async fn run_auction_tests() -> anyhow::Result<()> {
10661094
.expect("should not allow register/transfer multiple times");
10671095
it_can_batch_txs(&rig).await.expect("bump fee");
10681096
it_can_use_reserved_op_codes(&rig).await.expect("should use reserved opcodes");
1069-
it_should_allow_buy_sell(&rig).await.expect("should use reserved opcodes");
1097+
it_should_allow_buy_sell(&rig).await.expect("should allow buy sell");
1098+
it_should_allow_sign_verify_messages(&rig).await.expect("should sign verify");
10701099

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

0 commit comments

Comments
 (0)