Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Solana]: Add Solana custom message signing #4134

Merged
merged 4 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.trustwallet.core.app.blockchains.solana

import com.google.protobuf.ByteString
import com.trustwallet.core.app.utils.toHex
import com.trustwallet.core.app.utils.toHexByteArray
import org.junit.Assert.assertEquals
import org.junit.Test
import wallet.core.jni.Base58
import wallet.core.java.AnySigner
import wallet.core.jni.CoinType.SOLANA
import wallet.core.jni.MessageSigner
import wallet.core.jni.proto.Common.SigningError
import wallet.core.jni.proto.Solana

class TestSolanaMessageSigner {
init {
System.loadLibrary("TrustWalletCore")
}

@Test
fun testMessageSign() {
val signingInput = Solana.MessageSigningInput.newBuilder().apply {
privateKey = ByteString.copyFrom("44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d".toHexByteArray())
message = "Hello world"
}.build()

val outputData = MessageSigner.sign(SOLANA, signingInput.toByteArray())
val output = Solana.MessageSigningOutput.parseFrom(outputData)

assertEquals(output.error, SigningError.OK)
assertEquals(output.signature, "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG")
}

@Test
fun testMessageVerify() {
val verifyingInput = Solana.MessageVerifyingInput.newBuilder().apply {
publicKey = ByteString.copyFrom("ee6d61a89fc8f9909585a996bb0d2b2ac69ae23b5acf39a19f32631239ba06f9".toHexByteArray())
signature = "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG"
message = "Hello world"
}.build()

assert(MessageSigner.verify(SOLANA, verifyingInput.toByteArray()))
}
}
33 changes: 33 additions & 0 deletions include/TrustWalletCore/TWMessageSigner.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

#pragma once

#include "TWBase.h"
#include "TWPrivateKey.h"
#include "TWString.h"

TW_EXTERN_C_BEGIN

/// Represents a message signer to sign custom messages for any blockchain.
TW_EXPORT_CLASS
struct TWMessageSigner;

/// Signs an arbitrary message to prove ownership of an address for off-chain services.
///
/// \param coin The given coin type to sign the message for.
/// \param input The serialized data of a `MessageSigningInput` proto object, (e.g. `TW.Solana.Proto.MessageSigningInput`).
/// \return The serialized data of a `MessageSigningOutput` proto object, (e.g. `TW.Solana.Proto.MessageSigningOutput`).
TW_EXPORT_STATIC_METHOD
TWData* _Nullable TWMessageSignerSign(enum TWCoinType coin, TWData* _Nonnull input);

/// Verifies a signature for a message.
///
/// \param coin The given coin type to sign the message for.
/// \param input The serialized data of a verifying input (e.g. TW.Ethereum.Proto.MessageVerifyingInput).
/// \return whether the signature is valid.
TW_EXPORT_STATIC_METHOD
bool TWMessageSignerVerify(enum TWCoinType coin, TWData* _Nonnull input);

TW_EXTERN_C_END
1 change: 1 addition & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/chains/tw_solana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ tw_encoding = { path = "../../tw_encoding" }
tw_hash = { path = "../../tw_hash" }
tw_keypair = { path = "../../tw_keypair" }
tw_memory = { path = "../../tw_memory" }
tw_misc = { path = "../../tw_misc" }
tw_proto = { path = "../../tw_proto" }
9 changes: 7 additions & 2 deletions rust/chains/tw_solana/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use crate::address::SolanaAddress;
use crate::compiler::SolanaCompiler;
use crate::modules::offchain_message_signer::OffchainMessageSigner;
use crate::modules::transaction_decoder::SolanaTransactionDecoder;
use crate::modules::transaction_util::SolanaTransactionUtil;
use crate::modules::wallet_connect::connector::SolanaWalletConnector;
Expand All @@ -14,7 +15,6 @@ use tw_coin_entry::coin_entry::{CoinEntry, PublicKeyBytes, SignatureBytes};
use tw_coin_entry::derivation::Derivation;
use tw_coin_entry::error::prelude::*;
use tw_coin_entry::modules::json_signer::NoJsonSigner;
use tw_coin_entry::modules::message_signer::NoMessageSigner;
use tw_coin_entry::modules::plan_builder::NoPlanBuilder;
use tw_coin_entry::prefix::NoPrefix;
use tw_keypair::tw::PublicKey;
Expand All @@ -32,7 +32,7 @@ impl CoinEntry for SolanaEntry {
// Optional modules:
type JsonSigner = NoJsonSigner;
type PlanBuilder = NoPlanBuilder;
type MessageSigner = NoMessageSigner;
type MessageSigner = OffchainMessageSigner;
type WalletConnector = SolanaWalletConnector;
type TransactionDecoder = SolanaTransactionDecoder;
type TransactionUtil = SolanaTransactionUtil;
Expand Down Expand Up @@ -88,6 +88,11 @@ impl CoinEntry for SolanaEntry {
SolanaCompiler::compile(coin, input, signatures, public_keys)
}

#[inline]
fn message_signer(&self) -> Option<Self::MessageSigner> {
Some(OffchainMessageSigner)
}

#[inline]
fn wallet_connector(&self) -> Option<Self::WalletConnector> {
Some(SolanaWalletConnector)
Expand Down
1 change: 1 addition & 0 deletions rust/chains/tw_solana/src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod insert_instruction;
pub mod instruction_builder;
pub mod message_builder;
pub mod message_decompiler;
pub mod offchain_message_signer;
pub mod proto_builder;
pub mod transaction_decoder;
pub mod transaction_util;
Expand Down
75 changes: 75 additions & 0 deletions rust/chains/tw_solana/src/modules/offchain_message_signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use crate::SOLANA_ALPHABET;
use tw_coin_entry::coin_context::CoinContext;
use tw_coin_entry::error::prelude::*;
use tw_coin_entry::modules::message_signer::MessageSigner;
use tw_coin_entry::signing_output_error;
use tw_encoding::base58;
use tw_keypair::ed25519;
use tw_keypair::traits::{SigningKeyTrait, VerifyingKeyTrait};
use tw_misc::try_or_false;
use tw_proto::Solana::Proto;
use tw_proto::TxCompiler::Proto as CompilerProto;

/// Currently, supports https://solana.com/developers/cookbook/wallets/sign-message only.
pub struct OffchainMessageSigner;

impl OffchainMessageSigner {
pub fn sign_message_impl(
_coin: &dyn CoinContext,
input: Proto::MessageSigningInput,
) -> SigningResult<Proto::MessageSigningOutput<'static>> {
let private_key = ed25519::sha512::PrivateKey::try_from(input.private_key.as_ref())?;
let sign = private_key.sign(input.message.as_bytes().to_vec())?;
let base58_sign = base58::encode(sign.to_bytes().as_slice(), SOLANA_ALPHABET);
Ok(Proto::MessageSigningOutput {
signature: base58_sign.into(),
..Proto::MessageSigningOutput::default()
})
}
}

impl MessageSigner for OffchainMessageSigner {
type MessageSigningInput<'a> = Proto::MessageSigningInput<'a>;
type MessagePreSigningOutput = CompilerProto::PreSigningOutput<'static>;
type MessageSigningOutput = Proto::MessageSigningOutput<'static>;
type MessageVerifyingInput<'a> = Proto::MessageVerifyingInput<'a>;

fn message_preimage_hashes(
&self,
_coin: &dyn CoinContext,
input: Self::MessageSigningInput<'_>,
) -> Self::MessagePreSigningOutput {
CompilerProto::PreSigningOutput {
data_hash: input.message.as_bytes().to_vec().into(),
satoshiotomakan marked this conversation as resolved.
Show resolved Hide resolved
data: input.message.as_bytes().to_vec().into(),
..CompilerProto::PreSigningOutput::default()
}
}

fn sign_message(
&self,
coin: &dyn CoinContext,
input: Self::MessageSigningInput<'_>,
) -> Self::MessageSigningOutput {
Self::sign_message_impl(coin, input)
.unwrap_or_else(|e| signing_output_error!(Proto::MessageSigningOutput, e))
}

fn verify_message(
&self,
_coin: &dyn CoinContext,
input: Self::MessageVerifyingInput<'_>,
) -> bool {
let sign = try_or_false!(base58::decode(&input.signature, SOLANA_ALPHABET));
let sign = try_or_false!(ed25519::Signature::try_from(sign.as_slice()));
let public_key = try_or_false!(ed25519::sha512::PublicKey::try_from(
input.public_key.as_ref()
));
let message_utf8 = input.message.as_bytes().to_vec();
public_key.verify(sign, message_utf8)
}
}
18 changes: 9 additions & 9 deletions rust/tw_any_coin/src/ffi/tw_message_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ use tw_memory::ffi::tw_data::TWData;
use tw_memory::ffi::RawPtrTrait;
use tw_misc::{try_or_else, try_or_false};

/// Signs a message for the given blockchain.
/// Signs an arbitrary message to prove ownership of an address for off-chain services.
///
/// \param coin The given coin type to sign the message for.
/// \param input The serialized data of a signing input (e.g. TW.Ethereum.Proto.MessageSigningInput).
/// \param coin The given coin type to sign the transaction for.
/// \return The serialized data of a `SigningOutput` proto object. (e.g. TW.Ethereum.Proto.MessageSigningOutput).
#[no_mangle]
pub unsafe extern "C" fn tw_message_signer_sign(input: *const TWData, coin: u32) -> *mut TWData {
pub unsafe extern "C" fn tw_message_signer_sign(coin: u32, input: *const TWData) -> *mut TWData {
let input = try_or_else!(TWData::from_ptr_as_ref(input), std::ptr::null_mut);
let coin = try_or_else!(CoinType::try_from(coin), std::ptr::null_mut);

Expand All @@ -27,25 +27,25 @@ pub unsafe extern "C" fn tw_message_signer_sign(input: *const TWData, coin: u32)

/// Verifies a signature for a message.
///
/// \param input The serialized data of a signing input (e.g. TW.Ethereum.Proto.MessageSigningInput).
/// \param coin The given coin type to sign the transaction for.
/// \return The serialized data of a `SigningOutput` proto object. (e.g. TW.Ethereum.Proto.MessageSigningOutput).
/// \param coin The given coin type to sign the message for.
/// \param input The serialized data of a verifying input (e.g. TW.Ethereum.Proto.MessageVerifyingInput).
/// \return whether the signature is valid.
#[no_mangle]
pub unsafe extern "C" fn tw_message_signer_verify(input: *const TWData, coin: u32) -> bool {
pub unsafe extern "C" fn tw_message_signer_verify(coin: u32, input: *const TWData) -> bool {
let input = try_or_false!(TWData::from_ptr_as_ref(input));
let coin = try_or_false!(CoinType::try_from(coin));
MessageSigner::verify_message(input.as_slice(), coin).unwrap_or_default()
}

/// Computes preimage hashes of a message.
///
/// \param coin The given coin type to sign the message for.
/// \param input The serialized data of a signing input (e.g. TW.Ethereum.Proto.MessageSigningInput).
/// \param coin The given coin type to sign the transaction for.
/// \return The serialized data of TW.TxCompiler.PreSigningOutput.
#[no_mangle]
pub unsafe extern "C" fn tw_message_signer_pre_image_hashes(
input: *const TWData,
coin: u32,
input: *const TWData,
) -> *mut TWData {
let input = try_or_else!(TWData::from_ptr_as_ref(input), std::ptr::null_mut);
let coin = try_or_else!(CoinType::try_from(coin), std::ptr::null_mut);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fn test_tw_message_signer_sign() {

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let output = TWDataHelper::wrap(unsafe {
tw_message_signer_sign(input_data.ptr(), CoinType::Ethereum as u32)
tw_message_signer_sign(CoinType::Ethereum as u32, input_data.ptr())
})
.to_vec()
.expect("!tw_message_signer_sign returned nullptr");
Expand All @@ -45,7 +45,7 @@ fn test_tw_message_signer_verify() {
};

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let verified = unsafe { tw_message_signer_verify(input_data.ptr(), CoinType::Ethereum as u32) };
let verified = unsafe { tw_message_signer_verify(CoinType::Ethereum as u32, input_data.ptr()) };
assert!(verified);
}

Expand All @@ -58,7 +58,7 @@ fn test_tw_message_signer_verify_invalid() {
};

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let verified = unsafe { tw_message_signer_verify(input_data.ptr(), CoinType::Ethereum as u32) };
let verified = unsafe { tw_message_signer_verify(CoinType::Ethereum as u32, input_data.ptr()) };
assert!(!verified);
}

Expand All @@ -76,7 +76,7 @@ fn test_tw_message_signer_pre_image_hashes() {

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let output = TWDataHelper::wrap(unsafe {
tw_message_signer_pre_image_hashes(input_data.ptr(), CoinType::Ethereum as u32)
tw_message_signer_pre_image_hashes(CoinType::Ethereum as u32, input_data.ptr())
})
.to_vec()
.expect("!tw_message_signer_sign returned nullptr");
Expand Down
1 change: 1 addition & 0 deletions rust/tw_tests/tests/chains/solana/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
mod solana_address;
mod solana_address_ffi;
mod solana_compile;
mod solana_message_sign;
mod solana_sign;
mod solana_transaction;
mod solana_transaction_ffi;
Expand Down
80 changes: 80 additions & 0 deletions rust/tw_tests/tests/chains/solana/solana_message_sign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright © 2017 Trust Wallet.

use tw_any_coin::ffi::tw_message_signer::{
tw_message_signer_pre_image_hashes, tw_message_signer_sign, tw_message_signer_verify,
};
use tw_coin_entry::error::prelude::SigningErrorType;
use tw_coin_registry::coin_type::CoinType;
use tw_encoding::hex::DecodeHex;
use tw_memory::test_utils::tw_data_helper::TWDataHelper;
use tw_proto::{deserialize, serialize, Solana, TxCompiler};

#[test]
fn test_solana_message_signer_sign() {
let input = Solana::Proto::MessageSigningInput {
private_key: "44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d"
.decode_hex()
.unwrap()
.into(),
message: "Hello world".into(),
};

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let output = TWDataHelper::wrap(unsafe {
tw_message_signer_sign(CoinType::Solana as u32, input_data.ptr())
})
.to_vec()
.expect("!tw_message_signer_sign returned nullptr");

let output: Solana::Proto::MessageSigningOutput = deserialize(&output).unwrap();
assert_eq!(output.error, SigningErrorType::OK);
assert!(output.error_message.is_empty());
assert_eq!(
output.signature,
"2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG"
);
}

#[test]
fn test_solana_message_signer_verify() {
let input = Solana::Proto::MessageVerifyingInput {
public_key: "ee6d61a89fc8f9909585a996bb0d2b2ac69ae23b5acf39a19f32631239ba06f9"
.decode_hex()
.unwrap()
.into(),
message: "Hello world".into(),
signature: "2iBZ6zrQRKHcbD8NWmm552gU5vGvh1dk3XV4jxnyEdRKm8up8AeQk1GFr9pJokSmchw7i9gMtNyFBdDt8tBxM1cG".into(),
};

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let verified = unsafe { tw_message_signer_verify(CoinType::Solana as u32, input_data.ptr()) };
assert!(verified);
}

#[test]
fn test_solana_message_signer_pre_image_hashes() {
let message = "Hello world";

let input = Solana::Proto::MessageSigningInput {
private_key: "44f480ca27711895586074a14c552e58cc52e66a58edb6c58cf9b9b7295d4a2d"
.decode_hex()
.unwrap()
.into(),
message: message.into(),
};

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let output = TWDataHelper::wrap(unsafe {
tw_message_signer_pre_image_hashes(CoinType::Solana as u32, input_data.ptr())
})
.to_vec()
.expect("!tw_message_signer_sign returned nullptr");

let output: TxCompiler::Proto::PreSigningOutput = deserialize(&output).unwrap();
assert_eq!(output.error, SigningErrorType::OK);
assert!(output.error_message.is_empty());
let actual_message = String::from_utf8(output.data.to_vec()).unwrap();
assert_eq!(actual_message, message);
}
Loading
Loading