Skip to content

Commit

Permalink
Add wallet commands to sign and verify arbitrary messages
Browse files Browse the repository at this point in the history
  • Loading branch information
OBorce committed Feb 5, 2024
1 parent 83e7d86 commit 3eb14dd
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ impl SignedArbitraryMessage {
Self { raw_signature }
}

pub fn to_hex(self) -> String {
hex::encode(self.raw_signature)
}

pub fn verify_signature(
&self,
chain_config: &ChainConfig,
Expand Down
12 changes: 12 additions & 0 deletions test/functional/test_framework/wallet_cli_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,18 @@ async def decommission_stake_pool_request(self, pool_id: str) -> str:
async def sign_raw_transaction(self, transaction: str) -> str:
return await self._write_command(f"account-sign-raw-transaction {transaction}\n")

async def sign_challenge_plain(self, message: str, address: str) -> str:
return await self._write_command(f'account-sign-challenge-plain "{message}" {address}\n')

async def sign_challenge_hex(self, message: str, address: str) -> str:
return await self._write_command(f'account-sign-challenge-hex "{message}" {address}\n')

async def verify_challenge_plain(self, message: str, signature: str, address: str) -> str:
return await self._write_command(f'verify-challenge-plain "{message}" {signature} {address}\n')

async def verify_challenge_hex(self, message: str, signature: str, address: str) -> str:
return await self._write_command(f'verify-challenge-hex "{message}" {signature} {address}\n')

async def submit_transaction(self, transaction: str) -> str:
return await self._write_command(f"node-submit-transaction {transaction}\n")

Expand Down
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class UnicodeOnWindowsError(ValueError):
'feature_db_reinit.py',
'feature_lmdb_backend_test.py',
'wallet_conflict.py',
'wallet_sign_message.py',
'wallet_tx_compose.py',
'wallet_data_deposit.py',
'wallet_submit_tx.py',
Expand Down
102 changes: 102 additions & 0 deletions test/functional/wallet_sign_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
# Copyright (c) 2023 RBB S.r.l
# Copyright (c) 2017-2021 The Bitcoin Core developers
# [email protected]
# SPDX-License-Identifier: MIT
# Licensed under the MIT License;
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Wallet sign arbitrary message test
Check that:
* We can create a new cold wallet,
* generate an address
* sign a random message
* open a different wallet and verify the signature
"""

from random import choice, randint
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal, assert_in
from test_framework.wallet_cli_controller import WalletCliController

import asyncio
import sys
import string

class WalletSignMessage(BitcoinTestFramework):

def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 1
self.extra_args = [[
"--blockprod-min-peers-to-produce-blocks=0",
]]

def setup_network(self):
self.setup_nodes()
self.sync_all(self.nodes[0:1])

def run_test(self):
if 'win32' in sys.platform:
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
asyncio.run(self.async_test())

async def async_test(self):
node = self.nodes[0]
use_hex = choice([True, False])
message = "".join([choice(string.digits + string.ascii_letters) for _ in range(randint(20, 40))])
if use_hex:
message = message.encode().hex()

async with WalletCliController(node, self.config, self.log, chain_config_args=["--chain-pos-netupgrades", "true", "--cold-wallet"]) as wallet:
# new cold wallet
await wallet.create_wallet("cold_wallet")

destination = await wallet.new_address()
if use_hex:
output = await wallet.sign_challenge_hex(message, destination)
else:
output = await wallet.sign_challenge_plain(message, destination)
assert_in("The generated hex encoded signature is", output)
signature = output.split('\n')[1]

await wallet.close_wallet()

# new hot wallet
await wallet.create_wallet("another_cold_wallet")

# try to sign the message with the new wallet should fail
if use_hex:
output = await wallet.sign_challenge_hex(message, destination)
else:
output = await wallet.sign_challenge_plain(message, destination)
assert_in("Destination does not belong to this wallet", output)

if use_hex:
output = await wallet.verify_challenge_hex(message, signature, destination)
else:
output = await wallet.verify_challenge_plain(message, signature, destination)
assert_in("The provided signature is correct", output)

# try to verify with wrong message
different_message = "".join([choice(string.digits + string.ascii_letters) for _ in range(randint(20, 40))])
if use_hex:
different_message = different_message.encode().hex()
if use_hex:
output = await wallet.verify_challenge_hex(different_message, signature, destination)
else:
output = await wallet.verify_challenge_plain(different_message, signature, destination)
assert_in("Signature verification failed", output)

if __name__ == '__main__':
WalletSignMessage().main()

2 changes: 1 addition & 1 deletion test/functional/wallet_tx_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def make_output(pub_key_bytes):
assert len(encoded_tx) < len(encoded_ptx)

output = await wallet.sign_raw_transaction(encoded_tx)
assert_in("The transaction has been fully signed signed", output)
assert_in("The transaction has been fully signed and is ready to be broadcast to network.", output)
signed_tx = output.split('\n')[2]

assert_in("The transaction was submitted successfully", await wallet.submit_transaction(signed_tx))
Expand Down
22 changes: 22 additions & 0 deletions wallet/src/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod utxo_selector;

use common::address::pubkeyhash::PublicKeyHash;
use common::chain::block::timestamp::BlockTimestamp;
use common::chain::signature::inputsig::arbitrary_message::SignedArbitraryMessage;
use common::chain::{AccountCommand, AccountOutPoint, AccountSpending, TransactionCreationError};
use common::primitives::id::WithId;
use common::primitives::{Idable, H256};
Expand Down Expand Up @@ -1234,6 +1235,27 @@ impl Account {
))
}

pub fn sign_challenge(
&self,
message: Vec<u8>,
destination: Destination,
db_tx: &impl WalletStorageReadUnlocked,
) -> WalletResult<SignedArbitraryMessage> {
let private_key = self
.key_chain
.get_private_key_for_destination(&destination, db_tx)?
.ok_or(WalletError::DestinationNotFromThisWallet)?
.private_key();

let sig = SignedArbitraryMessage::produce_uniparty_signature(
&private_key,
&destination,
&message,
)?;

Ok(sig)
}

pub fn sign_raw_transaction(
&self,
tx: TransactionToSign,
Expand Down
18 changes: 18 additions & 0 deletions wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ use crate::{Account, SendRequest};
pub use bip39::{Language, Mnemonic};
use common::address::{Address, AddressError};
use common::chain::block::timestamp::BlockTimestamp;
use common::chain::signature::inputsig::arbitrary_message::{
SignArbitraryMessageError, SignedArbitraryMessage,
};
use common::chain::signature::DestinationSigError;
use common::chain::tokens::{
make_token_id, IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance,
Expand Down Expand Up @@ -199,6 +202,10 @@ pub enum WalletError {
FullySignedTransactionInDecommissionReq,
#[error("Input cannot be signed")]
InputCannotBeSigned,
#[error("Destination does not belong to this wallet")]
DestinationNotFromThisWallet,
#[error("Sign message error: {0}")]
SignMessageError(#[from] SignArbitraryMessageError),
#[error("Input cannot be spent {0:?}")]
InputCannotBeSpent(TxOutput),
#[error("Failed to convert partially signed tx to signed")]
Expand Down Expand Up @@ -1373,6 +1380,17 @@ impl<B: storage::Backend> Wallet<B> {
})
}

pub fn sign_challenge(
&mut self,
account_index: U31,
challenge: Vec<u8>,
destination: Destination,
) -> WalletResult<SignedArbitraryMessage> {
self.for_account_rw_unlocked(account_index, |account, db_tx, _| {
account.sign_challenge(challenge, destination, db_tx)
})
}

pub fn get_pos_gen_block_data(
&self,
account_index: U31,
Expand Down
111 changes: 111 additions & 0 deletions wallet/wallet-cli-lib/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,46 @@ pub enum ColdWalletCommand {
transaction: String,
},

/// Signs a challenge with a private key corresponding to the provided address destination.
#[clap(name = "account-sign-challenge-hex")]
SignChallegeHex {
/// Hex encoded message to be signed
message: String,
/// Address with whose private key to sign the challenge
address: String,
},

/// Signs a challenge with a private key corresponding to the provided address destination.
#[clap(name = "account-sign-challenge-plain")]
SignChallege {
/// The message to be signed
message: String,
/// Address with whose private key to sign the challenge
address: String,
},

/// Verifies a signed challenge against an address destination
#[clap(name = "verify-challenge-hex")]
VerifyChallengeHex {
/// The hex encoded message that was signed
message: String,
/// Hex encoded signed challenge
signed_challenge: String,
/// Address with whose private key the challenge was signed with
address: String,
},

/// Verifies a signed challenge against an address destination
#[clap(name = "verify-challenge-plain")]
VerifyChallenge {
/// The message that was signed
message: String,
/// Hex encoded signed challenge
signed_challenge: String,
/// Address with whose private key the challenge was signed with
address: String,
},

/// Print command history in the wallet for this execution
#[clap(name = "history-print")]
PrintHistory,
Expand Down Expand Up @@ -1094,6 +1134,77 @@ where
Ok(ConsoleCommand::Print(output_str))
}

ColdWalletCommand::SignChallegeHex {
message: challenge,
address,
} => {
let selected_account = self.get_selected_acc()?;
let challenge = hex::decode(challenge)
.map_err(|err| WalletCliError::InvalidInput(err.to_string()))?;
let result =
self.wallet_rpc.sign_challenge(selected_account, challenge, address).await?;

Ok(ConsoleCommand::Print(format!(
"The generated hex encoded signature is\n{}",
result.to_hex()
)))
}

ColdWalletCommand::SignChallege {
message: challenge,
address,
} => {
let selected_account = self.get_selected_acc()?;
let result = self
.wallet_rpc
.sign_challenge(selected_account, challenge.into_bytes(), address)
.await?;

Ok(ConsoleCommand::Print(format!(
"The generated hex encoded signature is\n{}",
result.to_hex()
)))
}

ColdWalletCommand::VerifyChallengeHex {
message,
signed_challenge,
address,
} => {
let message = hex::decode(message).map_err(|e| {
WalletCliError::InvalidInput(format!("invalid hex data: {}", e))
})?;
let signed_challenge = hex::decode(signed_challenge).map_err(|e| {
WalletCliError::InvalidInput(format!("invalid hex data: {}", e))
})?;

self.wallet_rpc.verify_challenge(message, signed_challenge, address)?;

Ok(ConsoleCommand::Print(
"The provided signature is correct".to_string(),
))
}

ColdWalletCommand::VerifyChallenge {
message,
signed_challenge,
address,
} => {
let signed_challenge = hex::decode(signed_challenge).map_err(|e| {
WalletCliError::InvalidInput(format!("invalid hex data: {}", e))
})?;

self.wallet_rpc.verify_challenge(
message.into_bytes(),
signed_challenge,
address,
)?;

Ok(ConsoleCommand::Print(
"The provided signature is correct".to_string(),
))
}

ColdWalletCommand::Version => Ok(ConsoleCommand::Print(get_version())),
ColdWalletCommand::Exit => {
if let Some(rpc) = self.server_rpc.take() {
Expand Down
11 changes: 11 additions & 0 deletions wallet/wallet-controller/src/synced_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use std::collections::BTreeSet;
use common::{
address::Address,
chain::{
signature::inputsig::arbitrary_message::SignedArbitraryMessage,
tokens::{
IsTokenFreezable, IsTokenUnfreezable, Metadata, RPCTokenInfo, TokenId, TokenIssuance,
TokenIssuanceV1, TokenTotalSupply,
Expand Down Expand Up @@ -640,6 +641,16 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> {
.map_err(ControllerError::WalletError)
}

pub fn sign_challenge(
&mut self,
challenge: Vec<u8>,
destination: Destination,
) -> Result<SignedArbitraryMessage, ControllerError<T>> {
self.wallet
.sign_challenge(self.account_index, challenge, destination)
.map_err(ControllerError::WalletError)
}

async fn get_current_and_consolidation_fee_rate(
&mut self,
) -> Result<(mempool::FeeRate, mempool::FeeRate), ControllerError<T>> {
Expand Down
Loading

0 comments on commit 3eb14dd

Please sign in to comment.