Add off-chain messages support (#27456)
This commit is contained in:
parent
28a89a1d99
commit
108a02cfd4
|
@ -6256,6 +6256,7 @@ dependencies = [
|
|||
"memmap2",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"num_enum",
|
||||
"pbkdf2 0.11.0",
|
||||
"qstring",
|
||||
"rand 0.7.3",
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -339,6 +339,7 @@ module.exports = {
|
|||
"offline-signing",
|
||||
"offline-signing/durable-nonce",
|
||||
"cli/usage",
|
||||
"cli/sign-offchain-message",
|
||||
],
|
||||
architectureSidebar: [
|
||||
{
|
||||
|
|
|
@ -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).
|
|
@ -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
|
||||
|
|
|
@ -5567,6 +5567,7 @@ dependencies = [
|
|||
"memmap2",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"num_enum",
|
||||
"pbkdf2 0.11.0",
|
||||
"qstring",
|
||||
"rand 0.7.3",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue