Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add initial abstraction for signers and wallets (#40)
* Add initial abstraction for signers and wallets Two new main abstractions are introduced here: signers and wallets. Wallets can be created from a secret key and used to sign messages and, eventually, transactions. The signer trait is generic enough to enable different sources to be signers, such as local wallets, trezor, ledger, metamask, etc. These changes enable only the creation of local wallets. All encryption is done through `secp256k1`. Here's an example of the new capability: ```Rust // Generate your secret key let mut rng = StdRng::seed_from_u64(2322u64); let mut secret_seed = [0u8; 32]; rng.fill_bytes(&mut secret_seed); let secret = SecretKey::from_slice(&secret_seed) .expect("Failed to generate random secret!"); // Create a new local wallet with the newly generated key let wallet = LocalWallet::new_from_private_key(secret)?; let message = "my message"; let signature = wallet.sign_message(message.as_bytes()).await?; // Recover address that signed the message let recovered_address = signature.recover(message).unwrap(); assert_eq!(wallet.address(), recovered_address); // Verify signature signature.verify(message, recovered_address).unwrap(); ```
- Loading branch information
Showing
9 changed files
with
428 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,5 +4,6 @@ members = [ | |
"fuels-abi-cli", | ||
"fuels-abigen-macro", | ||
"fuels-rs", | ||
"fuels-signers", | ||
"fuels-core", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
use crate::Bits256; | ||
use fuel_tx::{crypto::Hasher, Bytes64}; | ||
use fuel_types::Address; | ||
use fuel_vm::crypto::secp256k1_sign_compact_recover; | ||
use std::{convert::TryFrom, fmt, str::FromStr}; | ||
use thiserror::Error; | ||
|
||
/// An error involving a signature. | ||
#[derive(Debug, Error)] | ||
pub enum SignatureError { | ||
/// Invalid length | ||
#[error("invalid signature length, got {0}, expected 64")] | ||
InvalidLength(usize), | ||
/// When parsing a signature from string to hex | ||
#[error(transparent)] | ||
DecodingError(#[from] hex::FromHexError), | ||
/// Thrown when signature verification failed (i.e. when the address that | ||
/// produced the signature did not match the expected address) | ||
#[error("Signature verification failed. Expected {0}, got {1}")] | ||
VerificationError(Address, Address), | ||
/// Error in recovering public key from signature | ||
#[error("Public key recovery error")] | ||
RecoveryError, | ||
} | ||
|
||
/// Recovery message data. | ||
#[derive(Clone, Debug, PartialEq)] | ||
pub enum RecoveryMessage { | ||
/// Message bytes | ||
Data(Vec<u8>), | ||
/// Message hash | ||
Hash(Bits256), | ||
} | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq, Copy)] | ||
/// An ECDSA signature. Encoded as: | ||
/// | ||
/// ```plaintext | ||
/// | 32 bytes || 32 bytes | | ||
/// [256-bit r value][1-bit v value][255-bit s value] | ||
/// ``` | ||
/// | ||
/// The encoding of the signature was derived from | ||
/// [Compact Signature Representation](https://eips.ethereum.org/EIPS/eip-2098). | ||
/// | ||
/// Signatures are represented as the `r` and `s` (each 32 bytes), | ||
/// and `v` (1-bit) values of the signature. `r` and `s` take on | ||
/// their usual meaning while `v` is used for recovering the public | ||
/// key from a signature more quickly. | ||
pub struct Signature { | ||
pub compact: Bytes64, | ||
} | ||
|
||
impl fmt::Display for Signature { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "{}", hex::encode(self.compact)) | ||
} | ||
} | ||
|
||
impl Signature { | ||
/// Verifies that signature on `message` was produced by `address` | ||
pub fn verify<M, A>(&self, message: M, address: A) -> Result<(), SignatureError> | ||
where | ||
M: Into<RecoveryMessage>, | ||
A: Into<Address>, | ||
{ | ||
let address = address.into(); | ||
let recovered = self.recover(message)?; | ||
|
||
if recovered != address { | ||
return Err(SignatureError::VerificationError(address, recovered)); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Recovers the Fuel address which was used | ||
/// to sign the given message. Note that this message | ||
/// can be either the original message or its digest (hashed message). | ||
/// Both can be used to recover the address of the signature. | ||
pub fn recover<M>(&self, message: M) -> Result<Address, SignatureError> | ||
where | ||
M: Into<RecoveryMessage>, | ||
{ | ||
let message = message.into(); | ||
let message_hash = match message { | ||
RecoveryMessage::Data(ref message) => Hasher::hash(&message[..]), | ||
RecoveryMessage::Hash(hash) => hash.into(), | ||
}; | ||
|
||
let recovered = | ||
secp256k1_sign_compact_recover(self.compact.as_ref(), message_hash.as_ref()).unwrap(); | ||
|
||
let hashed = Hasher::hash(recovered); | ||
|
||
Ok(Address::new(*hashed)) | ||
} | ||
} | ||
|
||
impl<'a> TryFrom<&'a [u8]> for Signature { | ||
type Error = SignatureError; | ||
|
||
fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> { | ||
if bytes.len() != 64 { | ||
return Err(SignatureError::InvalidLength(bytes.len())); | ||
} | ||
|
||
Ok(Signature { | ||
compact: unsafe { Bytes64::from_slice_unchecked(bytes) }, | ||
}) | ||
} | ||
} | ||
|
||
impl FromStr for Signature { | ||
type Err = SignatureError; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
let s = s.strip_prefix("0x").unwrap_or(s); | ||
let bytes = hex::decode(s)?; | ||
Signature::try_from(&bytes[..]) | ||
} | ||
} | ||
|
||
impl From<&[u8]> for RecoveryMessage { | ||
fn from(s: &[u8]) -> Self { | ||
s.to_owned().into() | ||
} | ||
} | ||
|
||
impl From<Vec<u8>> for RecoveryMessage { | ||
fn from(s: Vec<u8>) -> Self { | ||
RecoveryMessage::Data(s) | ||
} | ||
} | ||
|
||
impl From<&str> for RecoveryMessage { | ||
fn from(s: &str) -> Self { | ||
s.as_bytes().to_owned().into() | ||
} | ||
} | ||
|
||
impl From<String> for RecoveryMessage { | ||
fn from(s: String) -> Self { | ||
RecoveryMessage::Data(s.into_bytes()) | ||
} | ||
} | ||
|
||
impl From<[u8; 32]> for RecoveryMessage { | ||
fn from(hash: [u8; 32]) -> Self { | ||
RecoveryMessage::Hash(hash) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn verify() { | ||
let msg = RecoveryMessage::Data("Some data".into()); | ||
|
||
let address = | ||
Address::from_str("0x014587212741268ad0b1bc727efce9711dbde69c484a9db38bd83bb1b3017c05") | ||
.unwrap(); | ||
|
||
let signature = Signature::from_str( | ||
"64d8b60c08a7ecab307cb11a31a7153ec7e4ff06a8fb78b4fe9c982d44c731efe63303ec5c7686a56445bacdd4ee89f592f1b3e68bded25ea404cd6806205db4" | ||
).expect("could not parse signature"); | ||
|
||
signature.verify(msg, address).unwrap(); | ||
} | ||
|
||
#[test] | ||
fn recover_signature() { | ||
let signature = Signature::from_str( | ||
"64d8b60c08a7ecab307cb11a31a7153ec7e4ff06a8fb78b4fe9c982d44c731efe63303ec5c7686a56445bacdd4ee89f592f1b3e68bded25ea404cd6806205db4" | ||
).expect("could not parse signature"); | ||
|
||
assert_eq!( | ||
signature.recover("Some data").unwrap(), | ||
Address::from_str("0x014587212741268ad0b1bc727efce9711dbde69c484a9db38bd83bb1b3017c05") | ||
.unwrap() | ||
); | ||
} | ||
|
||
#[test] | ||
fn signature_from_str() { | ||
let s1 = Signature::from_str( | ||
"0x64d8b60c08a7ecab307cb11a31a7153ec7e4ff06a8fb78b4fe9c982d44c731efe63303ec5c7686a56445bacdd4ee89f592f1b3e68bded25ea404cd6806205db4" | ||
).expect("could not parse 0x-prefixed signature"); | ||
|
||
let s2 = Signature::from_str( | ||
"64d8b60c08a7ecab307cb11a31a7153ec7e4ff06a8fb78b4fe9c982d44c731efe63303ec5c7686a56445bacdd4ee89f592f1b3e68bded25ea404cd6806205db4" | ||
).expect("could not parse non-prefixed signature"); | ||
|
||
assert_eq!(s1, s2); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
[package] | ||
name = "fuels-signers" | ||
version = "0.1.2" | ||
authors = ["Fuel Labs <contact@fuel.sh>"] | ||
edition = "2021" | ||
homepage = "https://fuel.network/" | ||
license = "Apache-2.0" | ||
repository = "https://github.com/FuelLabs/fuels-rs" | ||
description = "Fuel Rust SDK signers." | ||
|
||
[dependencies] | ||
async-trait = { version = "0.1.50", default-features = false } | ||
bytes = { version = "1.1.0", features = ["serde"] } | ||
fuel-tx = "0.1" | ||
fuel-types = { version = "0.1", default-features = false } | ||
fuel-vm = "0.1" | ||
fuels-core = { version = "0.1.2", path = "../fuels-core" } | ||
hex = { version = "0.4.3", default-features = false, features = ["std"] } | ||
rand = { version = "0.8.4", default-features = false } | ||
secp256k1 = { version = "0.20", features = ["recovery"] } | ||
serde = { version = "1.0.124", default-features = true, features = ["derive"] } | ||
sha2 = { version = "0.9.8", default-features = false } | ||
thiserror = { version = "1.0.30", default-features = false } | ||
tokio = { version = "1.10.1", features = ["full"] } | ||
|
||
[dev-dependencies] | ||
hex = { version = "0.4.3", default-features = false, features = ["std"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
mod wallet; | ||
|
||
pub use wallet::Wallet; | ||
|
||
use async_trait::async_trait; | ||
use fuel_tx::Transaction; | ||
use fuel_types::Address; | ||
use fuels_core::signature::Signature; | ||
use std::error::Error; | ||
|
||
/// A wallet instantiated with a locally stored private key | ||
pub type LocalWallet = Wallet; | ||
|
||
/// Trait for signing transactions and messages | ||
/// | ||
/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. | ||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] | ||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)] | ||
pub trait Signer: std::fmt::Debug + Send + Sync { | ||
type Error: Error + Send + Sync; | ||
/// Signs the hash of the provided message | ||
async fn sign_message<S: Send + Sync + AsRef<[u8]>>( | ||
&self, | ||
message: S, | ||
) -> Result<Signature, Self::Error>; | ||
|
||
/// Signs the transaction | ||
async fn sign_transaction(&self, message: &Transaction) -> Result<Signature, Self::Error>; | ||
|
||
/// Returns the signer's Fuel Address | ||
fn address(&self) -> Address; | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::str::FromStr; | ||
|
||
use rand::{rngs::StdRng, RngCore, SeedableRng}; | ||
use secp256k1::SecretKey; | ||
|
||
use super::*; | ||
|
||
#[tokio::test] | ||
async fn sign_and_verify() { | ||
let mut rng = StdRng::seed_from_u64(2322u64); | ||
let mut secret_seed = [0u8; 32]; | ||
rng.fill_bytes(&mut secret_seed); | ||
|
||
let secret = | ||
SecretKey::from_slice(&secret_seed).expect("Failed to generate random secret!"); | ||
|
||
let wallet = LocalWallet::new_from_private_key(secret).unwrap(); | ||
|
||
let message = "my message"; | ||
|
||
let signature = wallet.sign_message(message.as_bytes()).await.unwrap(); | ||
|
||
// Check if signature is what we expect it to be | ||
assert_eq!(signature.compact, Signature::from_str("0x8eeb238db1adea4152644f1cd827b552dfa9ab3f4939718bb45ca476d167c6512a656f4d4c7356bfb9561b14448c230c6e7e4bd781df5ee9e5999faa6495163d").unwrap().compact); | ||
|
||
// Recover address that signed the message | ||
let recovered_address = signature.recover(message).unwrap(); | ||
|
||
assert_eq!(wallet.address, recovered_address); | ||
|
||
// Verify signature | ||
signature.verify(message, recovered_address).unwrap(); | ||
} | ||
} |
Oops, something went wrong.