Add off-chain messages support (#27456)

This commit is contained in:
Alexey Skibin 2022-10-17 11:19:12 -04:00 committed by GitHub
parent 28a89a1d99
commit 108a02cfd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 698 additions and 4 deletions

1
Cargo.lock generated
View File

@ -6256,6 +6256,7 @@ dependencies = [
"memmap2",
"num-derive",
"num-traits",
"num_enum",
"pbkdf2 0.11.0",
"qstring",
"rand 0.7.3",

View File

@ -25,6 +25,7 @@ use {
decode_error::DecodeError,
hash::Hash,
instruction::InstructionError,
offchain_message::OffchainMessage,
pubkey::Pubkey,
signature::{Signature, Signer, SignerError},
stake::{instruction::LockupArgs, state::Lockup},
@ -440,6 +441,14 @@ pub enum CliCommand {
},
// Address lookup table commands
AddressLookupTable(AddressLookupTableCliCommand),
SignOffchainMessage {
message: OffchainMessage,
},
VerifyOffchainSignature {
signer_pubkey: Option<Pubkey>,
signature: Signature,
message: OffchainMessage,
},
}
#[derive(Debug, PartialEq)]
@ -470,6 +479,8 @@ pub enum CliError {
RpcRequestError(String),
#[error("Keypair file not found: {0}")]
KeypairFileNotFound(String),
#[error("Invalid signature")]
InvalidSignature,
}
impl From<Box<dyn error::Error>> for CliError {
@ -821,6 +832,12 @@ pub fn parse_command(
})
}
("transfer", Some(matches)) => parse_transfer(matches, default_signer, wallet_manager),
("sign-offchain-message", Some(matches)) => {
parse_sign_offchain_message(matches, default_signer, wallet_manager)
}
("verify-offchain-signature", Some(matches)) => {
parse_verify_offchain_signature(matches, default_signer, wallet_manager)
}
//
("", None) => {
eprintln!("{}", matches.usage());
@ -1634,11 +1651,18 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
derived_address_program_id.as_ref(),
compute_unit_price.as_ref(),
),
// Address Lookup Table Commands
CliCommand::AddressLookupTable(subcommand) => {
process_address_lookup_table_subcommand(rpc_client, config, subcommand)
}
CliCommand::SignOffchainMessage { message } => {
process_sign_offchain_message(config, message)
}
CliCommand::VerifyOffchainSignature {
signer_pubkey,
signature,
message,
} => process_verify_offchain_signature(config, signer_pubkey, signature, message),
}
}
@ -2013,7 +2037,7 @@ mod tests {
assert_eq!(
parse_command(&test_resolve_signer, &default_signer, &mut None).unwrap(),
CliCommandInfo {
command: CliCommand::ResolveSigner(Some(keypair_file)),
command: CliCommand::ResolveSigner(Some(keypair_file.clone())),
signers: vec![],
}
);
@ -2029,6 +2053,43 @@ mod tests {
signers: vec![],
}
);
// Test SignOffchainMessage
let test_sign_offchain = test_commands.clone().get_matches_from(vec![
"test",
"sign-offchain-message",
"Test Message",
]);
let message = OffchainMessage::new(0, b"Test Message").unwrap();
assert_eq!(
parse_command(&test_sign_offchain, &default_signer, &mut None).unwrap(),
CliCommandInfo {
command: CliCommand::SignOffchainMessage {
message: message.clone()
},
signers: vec![read_keypair_file(&keypair_file).unwrap().into()],
}
);
// Test VerifyOffchainSignature
let signature = keypair.sign_message(&message.serialize().unwrap());
let test_verify_offchain = test_commands.clone().get_matches_from(vec![
"test",
"verify-offchain-signature",
"Test Message",
&signature.to_string(),
]);
assert_eq!(
parse_command(&test_verify_offchain, &default_signer, &mut None).unwrap(),
CliCommandInfo {
command: CliCommand::VerifyOffchainSignature {
signer_pubkey: None,
signature,
message
},
signers: vec![read_keypair_file(&keypair_file).unwrap().into()],
}
);
}
#[test]
@ -2375,6 +2436,23 @@ mod tests {
config.command = CliCommand::GetTransactionCount;
assert!(process_command(&config).is_err());
let message = OffchainMessage::new(0, b"Test Message").unwrap();
config.command = CliCommand::SignOffchainMessage {
message: message.clone(),
};
config.signers = vec![&keypair];
let result = process_command(&config);
assert!(result.is_ok());
config.command = CliCommand::VerifyOffchainSignature {
signer_pubkey: None,
signature: result.unwrap().parse().unwrap(),
message,
};
config.signers = vec![&keypair];
let result = process_command(&config);
assert!(result.is_ok());
}
#[test]

View File

@ -33,6 +33,7 @@ use {
solana_sdk::{
commitment_config::CommitmentConfig,
message::Message,
offchain_message::OffchainMessage,
pubkey::Pubkey,
signature::Signature,
stake,
@ -274,6 +275,71 @@ impl WalletSubCommands for App<'_, '_> {
.arg(fee_payer_arg())
.arg(compute_unit_price_arg()),
)
.subcommand(
SubCommand::with_name("sign-offchain-message")
.about("Sign off-chain message")
.arg(
Arg::with_name("message")
.index(1)
.takes_value(true)
.value_name("STRING")
.required(true)
.help("The message text to be signed")
)
.arg(
Arg::with_name("version")
.long("version")
.takes_value(true)
.value_name("VERSION")
.required(false)
.default_value("0")
.validator(|p| match p.parse::<u8>() {
Err(_) => Err(String::from("Must be unsigned integer")),
Ok(_) => { Ok(()) }
})
.help("The off-chain message version")
)
)
.subcommand(
SubCommand::with_name("verify-offchain-signature")
.about("Verify off-chain message signature")
.arg(
Arg::with_name("message")
.index(1)
.takes_value(true)
.value_name("STRING")
.required(true)
.help("The text of the original message")
)
.arg(
Arg::with_name("signature")
.index(2)
.value_name("SIGNATURE")
.takes_value(true)
.required(true)
.help("The message signature to verify")
)
.arg(
Arg::with_name("version")
.long("version")
.takes_value(true)
.value_name("VERSION")
.required(false)
.default_value("0")
.validator(|p| match p.parse::<u8>() {
Err(_) => Err(String::from("Must be unsigned integer")),
Ok(_) => { Ok(()) }
})
.help("The off-chain message version")
)
.arg(
pubkey!(Arg::with_name("signer")
.long("signer")
.value_name("PUBKEY")
.required(false),
"The pubkey of the message signer (if different from config default)")
)
)
}
}
@ -447,6 +513,54 @@ pub fn parse_transfer(
})
}
pub fn parse_sign_offchain_message(
matches: &ArgMatches<'_>,
default_signer: &DefaultSigner,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let version: u8 = value_of(matches, "version").unwrap();
let message_text: String = value_of(matches, "message")
.ok_or_else(|| CliError::BadParameter("MESSAGE".to_string()))?;
let message = OffchainMessage::new(version, message_text.as_bytes())
.map_err(|_| CliError::BadParameter("VERSION or MESSAGE".to_string()))?;
Ok(CliCommandInfo {
command: CliCommand::SignOffchainMessage { message },
signers: vec![default_signer.signer_from_path(matches, wallet_manager)?],
})
}
pub fn parse_verify_offchain_signature(
matches: &ArgMatches<'_>,
default_signer: &DefaultSigner,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let version: u8 = value_of(matches, "version").unwrap();
let message_text: String = value_of(matches, "message")
.ok_or_else(|| CliError::BadParameter("MESSAGE".to_string()))?;
let message = OffchainMessage::new(version, message_text.as_bytes())
.map_err(|_| CliError::BadParameter("VERSION or MESSAGE".to_string()))?;
let signer_pubkey = pubkey_of_signer(matches, "signer", wallet_manager)?;
let signers = if signer_pubkey.is_some() {
vec![]
} else {
vec![default_signer.signer_from_path(matches, wallet_manager)?]
};
let signature = value_of(matches, "signature")
.ok_or_else(|| CliError::BadParameter("SIGNATURE".to_string()))?;
Ok(CliCommandInfo {
command: CliCommand::VerifyOffchainSignature {
signer_pubkey,
signature,
message,
},
signers,
})
}
pub fn process_show_account(
rpc_client: &RpcClient,
config: &CliConfig,
@ -779,3 +893,29 @@ pub fn process_transfer(
log_instruction_custom_error::<SystemError>(result, config)
}
}
pub fn process_sign_offchain_message(
config: &CliConfig,
message: &OffchainMessage,
) -> ProcessResult {
Ok(message.sign(config.signers[0])?.to_string())
}
pub fn process_verify_offchain_signature(
config: &CliConfig,
signer_pubkey: &Option<Pubkey>,
signature: &Signature,
message: &OffchainMessage,
) -> ProcessResult {
let signer = if let Some(pubkey) = signer_pubkey {
*pubkey
} else {
config.signers[0].pubkey()
};
if message.verify(&signer, signature)? {
Ok("Signature is valid".to_string())
} else {
Err(CliError::InvalidSignature.into())
}
}

View File

@ -339,6 +339,7 @@ module.exports = {
"offline-signing",
"offline-signing/durable-nonce",
"cli/usage",
"cli/sign-offchain-message",
],
architectureSidebar: [
{

View File

@ -0,0 +1,98 @@
---
title: Off-Chain Message Signing
---
Off-chain message signing is a method of signing non-transaction messages with
a Solana wallet. This feature can be used to authenticate users or provide
proof of wallet ownership.
## Sign Off-Chain Message
To sign an arbitrary off-chain message, run the following command:
```bash
solana sign-offchain-message <MESSAGE>
```
The message will be encoded and signed with CLI's default private key and
signature printed to the output. If you want to sign it with another key, just
use the `-k/--keypair` option:
```bash
solana sign-offchain-message -k <KEYPAIR> <MESSAGE>
```
By default, the messages constructed are version 0, the only version currently
supported. When other versions become available, you can override the default
value with the `--version` option:
```bash
solana sign-offchain-message -k <KEYPAIR> --version <VERSION> <MESSAGE>
```
The message format is determined automatically based on the version and text
of the message.
Version `0` headers specify three message formats allowing for trade-offs
between compatibility and composition of messages:
| ID | Encoding | Maximum Length | Hardware Wallet Support |
| :-: | :-----------------: | :------------: | :---------------------: |
| 0 | Restricted ASCII \* | 1212 | Yes |
| 1 | UTF-8 | 1212 | Blind sign only |
| 2 | UTF-8 | 65515 | No |
\* Those characters for which [`isprint(3)`](https://linux.die.net/man/3/isprint)
returns true. That is, `0x20..=0x7e`.
Formats `0` and `1` are motivated by hardware wallet support where both RAM to
store the payload and font character support are limited.
To sign an off-chain message with Ledger, ensure your Ledger is running latest
firmware and Solana Ledger App version 1.3.0 or later. After Ledger is
unlocked and Solana Ledger App is open, run:
```bash
solana sign-offchain-message -k usb://ledger <MESSAGE>
```
For more information on how to setup and work with the ledger device see this
[link](../wallet-guide/hardware-wallets/ledger.md).
Please note that UTF-8 encoded messages require `Allow blind sign` option
enabled in Solana Ledger App. Also, due to the lack of UTF-8 support in Ledger
devices, only the hash of the message will be displayed in such cases.
If `Display mode` is set to `Expert`, Ledger will display technical
information about the message to be signed.
## Verify Off-Chain Message Signature
To verify the off-chain message signature, run the following command:
```bash
solana verify-offchain-signature <MESSAGE> <SIGNATURE>
```
The public key of the default CLI signer will be used. You can specify another
key with the `--signer` option:
```bash
solana verify-offchain-signature --signer <PUBKEY> <MESSAGE> <SIGNATURE>
```
If the signed message has a version different from the default, you need to
specify the matching version explicitly:
```bash
solana verify-offchain-signature --version <VERSION> <MESSAGE> <SIGNATURE>
```
## Protocol Specification
To ensure that off-chain messages are not valid transactions, they are encoded
with a fixed prefix: `\xffsolana offchain`, where first byte is chosen such
that it is implicitly illegal as the first byte in a transaction
`MessageHeader` today. More details about the payload format and other
considerations are available in the
[proposal](https://github.com/solana-labs/solana/blob/e80f67dd58b7fa3901168055211f346164efa43a/docs/src/proposals/off-chain-message-signing.md).

View File

@ -2,7 +2,7 @@
title: Ledger Nano
---
This page describes how to use a Ledger Nano S or Nano X to interact with Solana
This page describes how to use a Ledger Nano S, Nano S Plus, or Nano X to interact with Solana
using the command line tools.
## Before You Begin

View File

@ -5567,6 +5567,7 @@ dependencies = [
"memmap2",
"num-derive",
"num-traits",
"num_enum",
"pbkdf2 0.11.0",
"qstring",
"rand 0.7.3",

View File

@ -67,6 +67,7 @@ mod commands {
pub const GET_APP_CONFIGURATION: u8 = 0x04;
pub const GET_PUBKEY: u8 = 0x05;
pub const SIGN_MESSAGE: u8 = 0x06;
pub const SIGN_OFFCHAIN_MESSAGE: u8 = 0x07;
}
enum ConfigurationVersion {
@ -430,6 +431,13 @@ impl RemoteWallet<hidapi::DeviceInfo> for LedgerWallet {
derivation_path: &DerivationPath,
data: &[u8],
) -> Result<Signature, RemoteWalletError> {
// If the first byte of the data is 0xff then it is an off-chain message
// because it starts with the Domain Specifier b"\xffsolana offchain".
// On-chain messages, in contrast, start with either 0x80 (MESSAGE_VERSION_PREFIX)
// or the number of signatures (0x00 - 0x13).
if !data.is_empty() && data[0] == 0xff {
return self.sign_offchain_message(derivation_path, data);
}
let mut payload = if self.outdated_app() {
extend_and_serialize(derivation_path)
} else {
@ -498,7 +506,16 @@ impl RemoteWallet<hidapi::DeviceInfo> for LedgerWallet {
chunks.last_mut().unwrap().0 &= !P2_MORE;
for (p2, payload) in chunks {
result = self.send_apdu(commands::SIGN_MESSAGE, p1, p2, &payload)?;
result = self.send_apdu(
if self.outdated_app() {
commands::DEPRECATED_SIGN_MESSAGE
} else {
commands::SIGN_MESSAGE
},
p1,
p2,
&payload,
)?;
}
}
@ -509,6 +526,42 @@ impl RemoteWallet<hidapi::DeviceInfo> for LedgerWallet {
}
Ok(Signature::new(&result))
}
fn sign_offchain_message(
&self,
derivation_path: &DerivationPath,
message: &[u8],
) -> Result<Signature, RemoteWalletError> {
if message.len()
> solana_sdk::offchain_message::v0::OffchainMessage::MAX_LEN_LEDGER
+ solana_sdk::offchain_message::v0::OffchainMessage::HEADER_LEN
{
return Err(RemoteWalletError::InvalidInput(
"Off-chain message to sign is too long".to_string(),
));
}
let mut data = extend_and_serialize_multiple(&[derivation_path]);
data.extend_from_slice(message);
let p1 = P1_CONFIRM;
let mut p2 = 0;
let mut payload = data.as_slice();
while payload.len() > MAX_CHUNK_SIZE {
let chunk = &payload[..MAX_CHUNK_SIZE];
self.send_apdu(commands::SIGN_OFFCHAIN_MESSAGE, p1, p2 | P2_MORE, chunk)?;
payload = &payload[MAX_CHUNK_SIZE..];
p2 |= P2_EXTEND;
}
let result = self.send_apdu(commands::SIGN_OFFCHAIN_MESSAGE, p1, p2, payload)?;
if result.len() != 64 {
return Err(RemoteWalletError::Protocol(
"Signature packet size mismatch",
));
}
Ok(Signature::new(&result))
}
}
/// Check if the detected device is a valid `Ledger device` by checking both the product ID and the vendor ID

View File

@ -71,6 +71,15 @@ pub enum LedgerError {
#[error("Ledger received invalid Solana message")]
SolanaInvalidMessage = 0x6a80,
#[error("Ledger received message with invalid header")]
SolanaInvalidMessageHeader = 0x6a81,
#[error("Ledger received message in invalid format")]
SolanaInvalidMessageFormat = 0x6a82,
#[error("Ledger received message with invalid size")]
SolanaInvalidMessageSize = 0x6a83,
#[error("Solana summary finalization failed on Ledger device")]
SolanaSummaryFinalizeFailed = 0x6f00,

View File

@ -236,6 +236,15 @@ pub trait RemoteWallet<T> {
) -> Result<Signature, RemoteWalletError> {
unimplemented!();
}
/// Sign off-chain message with wallet managing pubkey at derivation path m/44'/501'/<account>'/<change>'.
fn sign_offchain_message(
&self,
derivation_path: &DerivationPath,
message: &[u8],
) -> Result<Signature, RemoteWalletError> {
unimplemented!();
}
}
/// `RemoteWallet` device

View File

@ -60,6 +60,7 @@ log = "0.4.17"
memmap2 = { version = "0.5.3", optional = true }
num-derive = "0.3"
num-traits = "0.2"
num_enum = "0.5.7"
pbkdf2 = { version = "0.11.0", default-features = false }
qstring = "0.7.2"
rand = { version = "0.7.0", optional = true }

View File

@ -47,6 +47,7 @@ pub mod inflation;
pub mod log;
pub mod native_loader;
pub mod nonce_account;
pub mod offchain_message;
pub mod packet;
pub mod poh_config;
pub mod precompiles;

302
sdk/src/offchain_message.rs Normal file
View File

@ -0,0 +1,302 @@
//! Off-Chain Message container for storing non-transaction messages.
#![cfg(feature = "full")]
use {
crate::{
hash::Hash,
pubkey::Pubkey,
sanitize::SanitizeError,
signature::{Signature, Signer},
},
num_enum::{IntoPrimitive, TryFromPrimitive},
};
#[cfg(test)]
static_assertions::const_assert_eq!(OffchainMessage::HEADER_LEN, 17);
#[cfg(test)]
static_assertions::const_assert_eq!(v0::OffchainMessage::MAX_LEN, 65515);
#[cfg(test)]
static_assertions::const_assert_eq!(v0::OffchainMessage::MAX_LEN_LEDGER, 1212);
/// Check if given bytes contain only printable ASCII characters
pub fn is_printable_ascii(data: &[u8]) -> bool {
for &char in data {
if !(0x20..=0x7e).contains(&char) {
return false;
}
}
true
}
/// Check if given bytes contain valid UTF8 string
pub fn is_utf8(data: &[u8]) -> bool {
std::str::from_utf8(data).is_ok()
}
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, TryFromPrimitive, IntoPrimitive)]
pub enum MessageFormat {
RestrictedAscii,
LimitedUtf8,
ExtendedUtf8,
}
#[allow(clippy::integer_arithmetic)]
pub mod v0 {
use {
super::{is_printable_ascii, is_utf8, MessageFormat, OffchainMessage as Base},
crate::{
hash::{Hash, Hasher},
packet::PACKET_DATA_SIZE,
sanitize::SanitizeError,
},
};
/// OffchainMessage Version 0.
/// Struct always contains a non-empty valid message.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct OffchainMessage {
format: MessageFormat,
message: Vec<u8>,
}
impl OffchainMessage {
// Header Length = Message Format (1) + Message Length (2)
pub const HEADER_LEN: usize = 3;
// Max length of the OffchainMessage
pub const MAX_LEN: usize = u16::MAX as usize - Base::HEADER_LEN - Self::HEADER_LEN;
// Max Length of the OffchainMessage supported by the Ledger
pub const MAX_LEN_LEDGER: usize = PACKET_DATA_SIZE - Base::HEADER_LEN - Self::HEADER_LEN;
/// Construct a new OffchainMessage object from the given message
pub fn new(message: &[u8]) -> Result<Self, SanitizeError> {
let format = if message.is_empty() {
return Err(SanitizeError::InvalidValue);
} else if message.len() <= OffchainMessage::MAX_LEN_LEDGER {
if is_printable_ascii(message) {
MessageFormat::RestrictedAscii
} else if is_utf8(message) {
MessageFormat::LimitedUtf8
} else {
return Err(SanitizeError::InvalidValue);
}
} else if message.len() <= OffchainMessage::MAX_LEN {
if is_utf8(message) {
MessageFormat::ExtendedUtf8
} else {
return Err(SanitizeError::InvalidValue);
}
} else {
return Err(SanitizeError::ValueOutOfBounds);
};
Ok(Self {
format,
message: message.to_vec(),
})
}
/// Serialize the message to bytes, including the full header
pub fn serialize(&self, data: &mut Vec<u8>) -> Result<(), SanitizeError> {
// invalid messages shouldn't be possible, but a quick sanity check never hurts
assert!(!self.message.is_empty() && self.message.len() <= Self::MAX_LEN);
data.reserve(Self::HEADER_LEN.saturating_add(self.message.len()));
// format
data.push(self.format.into());
// message length
data.extend_from_slice(&(self.message.len() as u16).to_le_bytes());
// message
data.extend_from_slice(&self.message);
Ok(())
}
/// Deserialize the message from bytes that include a full header
pub fn deserialize(data: &[u8]) -> Result<Self, SanitizeError> {
// validate data length
if data.len() <= Self::HEADER_LEN || data.len() > Self::HEADER_LEN + Self::MAX_LEN {
return Err(SanitizeError::ValueOutOfBounds);
}
// decode header
let format =
MessageFormat::try_from(data[0]).map_err(|_| SanitizeError::InvalidValue)?;
let message_len = u16::from_le_bytes([data[1], data[2]]) as usize;
// check header
if Self::HEADER_LEN.saturating_add(message_len) != data.len() {
return Err(SanitizeError::InvalidValue);
}
let message = &data[Self::HEADER_LEN..];
// check format
let is_valid = match format {
MessageFormat::RestrictedAscii => {
(message.len() <= Self::MAX_LEN_LEDGER) && is_printable_ascii(message)
}
MessageFormat::LimitedUtf8 => {
(message.len() <= Self::MAX_LEN_LEDGER) && is_utf8(message)
}
MessageFormat::ExtendedUtf8 => (message.len() <= Self::MAX_LEN) && is_utf8(message),
};
if is_valid {
Ok(Self {
format,
message: message.to_vec(),
})
} else {
Err(SanitizeError::InvalidValue)
}
}
/// Compute the SHA256 hash of the serialized off-chain message
pub fn hash(serialized_message: &[u8]) -> Result<Hash, SanitizeError> {
let mut hasher = Hasher::default();
hasher.hash(serialized_message);
Ok(hasher.result())
}
pub fn get_format(&self) -> MessageFormat {
self.format
}
pub fn get_message(&self) -> &Vec<u8> {
&self.message
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum OffchainMessage {
V0(v0::OffchainMessage),
}
impl OffchainMessage {
pub const SIGNING_DOMAIN: &'static [u8] = b"\xffsolana offchain";
// Header Length = Signing Domain (16) + Header Version (1)
pub const HEADER_LEN: usize = Self::SIGNING_DOMAIN.len() + 1;
/// Construct a new OffchainMessage object from the given version and message
pub fn new(version: u8, message: &[u8]) -> Result<Self, SanitizeError> {
match version {
0 => Ok(Self::V0(v0::OffchainMessage::new(message)?)),
_ => Err(SanitizeError::ValueOutOfBounds),
}
}
/// Serialize the off-chain message to bytes including full header
pub fn serialize(&self) -> Result<Vec<u8>, SanitizeError> {
// serialize signing domain
let mut data = Self::SIGNING_DOMAIN.to_vec();
// serialize version and call version specific serializer
match self {
Self::V0(msg) => {
data.push(0);
msg.serialize(&mut data)?;
}
}
Ok(data)
}
/// Deserialize the off-chain message from bytes that include full header
pub fn deserialize(data: &[u8]) -> Result<Self, SanitizeError> {
if data.len() <= Self::HEADER_LEN {
return Err(SanitizeError::ValueOutOfBounds);
}
let version = data[Self::SIGNING_DOMAIN.len()];
let data = &data[Self::SIGNING_DOMAIN.len().saturating_add(1)..];
match version {
0 => Ok(Self::V0(v0::OffchainMessage::deserialize(data)?)),
_ => Err(SanitizeError::ValueOutOfBounds),
}
}
/// Compute the hash of the off-chain message
pub fn hash(&self) -> Result<Hash, SanitizeError> {
match self {
Self::V0(_) => v0::OffchainMessage::hash(&self.serialize()?),
}
}
pub fn get_version(&self) -> u8 {
match self {
Self::V0(_) => 0,
}
}
pub fn get_format(&self) -> MessageFormat {
match self {
Self::V0(msg) => msg.get_format(),
}
}
pub fn get_message(&self) -> &Vec<u8> {
match self {
Self::V0(msg) => msg.get_message(),
}
}
/// Sign the message with provided keypair
pub fn sign(&self, signer: &dyn Signer) -> Result<Signature, SanitizeError> {
Ok(signer.sign_message(&self.serialize()?))
}
/// Verify that the message signature is valid for the given public key
pub fn verify(&self, signer: &Pubkey, signature: &Signature) -> Result<bool, SanitizeError> {
Ok(signature.verify(signer.as_ref(), &self.serialize()?))
}
}
#[cfg(test)]
mod tests {
use {super::*, crate::signature::Keypair, std::str::FromStr};
#[test]
fn test_offchain_message_ascii() {
let message = OffchainMessage::new(0, b"Test Message").unwrap();
assert_eq!(message.get_version(), 0);
assert_eq!(message.get_format(), MessageFormat::RestrictedAscii);
assert_eq!(message.get_message().as_slice(), b"Test Message");
assert!(
matches!(message, OffchainMessage::V0(ref msg) if msg.get_format() == MessageFormat::RestrictedAscii)
);
let serialized = [
255, 115, 111, 108, 97, 110, 97, 32, 111, 102, 102, 99, 104, 97, 105, 110, 0, 0, 12, 0,
84, 101, 115, 116, 32, 77, 101, 115, 115, 97, 103, 101,
];
let hash = Hash::from_str("HG5JydBGjtjTfD3sSn21ys5NTWPpXzmqifiGC2BVUjkD").unwrap();
assert_eq!(message.serialize().unwrap(), serialized);
assert_eq!(message.hash().unwrap(), hash);
assert_eq!(message, OffchainMessage::deserialize(&serialized).unwrap());
}
#[test]
fn test_offchain_message_utf8() {
let message = OffchainMessage::new(0, "Тестовое сообщение".as_bytes()).unwrap();
assert_eq!(message.get_version(), 0);
assert_eq!(message.get_format(), MessageFormat::LimitedUtf8);
assert_eq!(
message.get_message().as_slice(),
"Тестовое сообщение".as_bytes()
);
assert!(
matches!(message, OffchainMessage::V0(ref msg) if msg.get_format() == MessageFormat::LimitedUtf8)
);
let serialized = [
255, 115, 111, 108, 97, 110, 97, 32, 111, 102, 102, 99, 104, 97, 105, 110, 0, 1, 35, 0,
208, 162, 208, 181, 209, 129, 209, 130, 208, 190, 208, 178, 208, 190, 208, 181, 32,
209, 129, 208, 190, 208, 190, 208, 177, 209, 137, 208, 181, 208, 189, 208, 184, 208,
181,
];
let hash = Hash::from_str("6GXTveatZQLexkX4WeTpJ3E7uk1UojRXpKp43c4ArSun").unwrap();
assert_eq!(message.serialize().unwrap(), serialized);
assert_eq!(message.hash().unwrap(), hash);
assert_eq!(message, OffchainMessage::deserialize(&serialized).unwrap());
}
#[test]
fn test_offchain_message_sign_and_verify() {
let message = OffchainMessage::new(0, b"Test Message").unwrap();
let keypair = Keypair::new();
let signature = message.sign(&keypair).unwrap();
assert!(message.verify(&keypair.pubkey(), &signature).unwrap());
}
}