Arbitrum Stylus logo

Stylus by Example

Verifying Signature

An Arbitrum Stylus version implementation of Verifying Signature.

Messages can be signed off chain and then verified on chain using a smart contract.

Example interact using ethers.js

src/lib.rs

1// Only run this as a WASM if the export-abi feature is not set.
2#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
3extern crate alloc;
4
5
6use alloy_primitives::FixedBytes;
7/// Import items from the SDK. The prelude contains common traits and macros.
8use stylus_sdk::{abi::Bytes, alloy_primitives::{address, Address, U256}, call::{self, Call}, prelude::*, crypto::keccak};
9use alloc::string::String;
10use alloy_sol_types::{sol_data::{Address as SOLAddress, FixedBytes as SolFixedBytes, *}, SolType, sol};
11
12type ECRECOVERType = (SolFixedBytes<32>, Uint<8>, SolFixedBytes<32>, SolFixedBytes<32>);
13
14sol!{
15    error EcrecoverCallError();
16    error InvalidSignatureLength();
17}
18
19// Define some persistent storage using the Solidity ABI.
20// `VerifySignature` will be the entrypoint.
21#[storage]  
22#[entrypoint]  
23pub struct VerifySignature;  
24
25
26#[derive(SolidityError)]
27pub enum VerifySignatureError {
28    EcrecoverCallError(EcrecoverCallError),
29    InvalidSignatureLength(InvalidSignatureLength),
30}
31
32const ECRECOVER: Address = address!("0000000000000000000000000000000000000001");
33const SIGNED_MESSAGE_HEAD: &'static str = "\x19Ethereum Signed Message:\n32";
34
35/// Declare that `VerifySignature` is a contract with the following external methods.
36#[public]
37impl VerifySignature {
38    /* 1. Unlock MetaMask account
39    ethereum.enable()
40    */
41
42    /* 2. Get message hash to sign
43    getMessageHash(
44        0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C,
45        123,
46        "coffee and donuts",
47        1
48    )
49
50    hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd"
51    */
52    pub fn get_message_hash(
53        &self,
54        to: Address,
55        amount: U256,
56        message: String,
57        nonce: U256,
58    ) -> FixedBytes<32> {
59        let message_data = [&to.to_vec(), &amount.to_be_bytes_vec(), message.as_bytes(), &nonce.to_be_bytes_vec()].concat();
60        keccak(message_data).into()
61    }
62
63    /* 3. Sign message hash
64    # using browser
65    account = "copy paste account of signer here"
66    ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log)
67
68    # using web3
69    web3.personal.sign(hash, web3.eth.defaultAccount, console.log)
70
71    Signature will be different for different accounts
72    0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
73    */
74    pub fn get_eth_signed_message_hash(&self, message_hash: FixedBytes<32>) -> FixedBytes<32> {
75        let message_to_be_decoded = [SIGNED_MESSAGE_HEAD.as_bytes(), &message_hash.to_vec()].concat();
76        keccak(message_to_be_decoded).into()
77    }
78
79    /* 4. Verify signature
80    signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
81    to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
82    amount = 123
83    message = "coffee and donuts"
84    nonce = 1
85    signature =
86        0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
87    */
88    pub fn verify(
89        &self,
90        signer: Address,
91        to: Address,
92        amount: U256,
93        message: String,
94        nonce: U256,
95        signature: Bytes,
96    ) -> Result<bool, VerifySignatureError> {
97        let message_hash = self.get_message_hash(to, amount, message, nonce);
98        let eth_signed_message_hash = self.get_eth_signed_message_hash(message_hash);
99        match self.recover_signer(eth_signed_message_hash, signature) {
100            Ok(recovered_signer) => Ok(recovered_signer == signer),
101            Err(err) => Err(err),
102        }
103    }
104
105    pub fn recover_signer(
106        &self,
107        eth_signed_message_hash: FixedBytes<32>,
108        signature: Bytes
109    ) -> Result<Address, VerifySignatureError> {
110        let (r, s, v) = self.split_signature(signature);
111        self.ecrecover_call(eth_signed_message_hash, v, r, s)
112    }
113
114    /// Invoke the ECRECOVER precompile.
115    pub fn ecrecover_call(
116        &self,
117        hash: FixedBytes<32>,
118        v: u8,
119        r: FixedBytes<32>,
120        s: FixedBytes<32>,
121    ) -> Result<Address, VerifySignatureError> {
122        let data = (hash, v, r, s);
123        let encoded_data = ECRECOVERType::abi_encode(&data);
124        match call::static_call(Call::new(), ECRECOVER, &encoded_data) {
125            Ok(result) => Ok(SOLAddress::abi_decode(&result, false).unwrap()),
126            Err(_) => Err(VerifySignatureError::EcrecoverCallError(EcrecoverCallError{})),
127        }
128    }
129
130
131    pub fn split_signature(
132        &self,
133        signature: Bytes
134    ) -> (FixedBytes<32>, FixedBytes<32>, u8) {
135        let r = FixedBytes::from_slice(&signature[0..32]);
136        let s = FixedBytes::from_slice(&signature[32..64]);
137        let v = signature[64];
138        (r, s, v)
139    }
140            
141}
1// Only run this as a WASM if the export-abi feature is not set.
2#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
3extern crate alloc;
4
5
6use alloy_primitives::FixedBytes;
7/// Import items from the SDK. The prelude contains common traits and macros.
8use stylus_sdk::{abi::Bytes, alloy_primitives::{address, Address, U256}, call::{self, Call}, prelude::*, crypto::keccak};
9use alloc::string::String;
10use alloy_sol_types::{sol_data::{Address as SOLAddress, FixedBytes as SolFixedBytes, *}, SolType, sol};
11
12type ECRECOVERType = (SolFixedBytes<32>, Uint<8>, SolFixedBytes<32>, SolFixedBytes<32>);
13
14sol!{
15    error EcrecoverCallError();
16    error InvalidSignatureLength();
17}
18
19// Define some persistent storage using the Solidity ABI.
20// `VerifySignature` will be the entrypoint.
21#[storage]  
22#[entrypoint]  
23pub struct VerifySignature;  
24
25
26#[derive(SolidityError)]
27pub enum VerifySignatureError {
28    EcrecoverCallError(EcrecoverCallError),
29    InvalidSignatureLength(InvalidSignatureLength),
30}
31
32const ECRECOVER: Address = address!("0000000000000000000000000000000000000001");
33const SIGNED_MESSAGE_HEAD: &'static str = "\x19Ethereum Signed Message:\n32";
34
35/// Declare that `VerifySignature` is a contract with the following external methods.
36#[public]
37impl VerifySignature {
38    /* 1. Unlock MetaMask account
39    ethereum.enable()
40    */
41
42    /* 2. Get message hash to sign
43    getMessageHash(
44        0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C,
45        123,
46        "coffee and donuts",
47        1
48    )
49
50    hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd"
51    */
52    pub fn get_message_hash(
53        &self,
54        to: Address,
55        amount: U256,
56        message: String,
57        nonce: U256,
58    ) -> FixedBytes<32> {
59        let message_data = [&to.to_vec(), &amount.to_be_bytes_vec(), message.as_bytes(), &nonce.to_be_bytes_vec()].concat();
60        keccak(message_data).into()
61    }
62
63    /* 3. Sign message hash
64    # using browser
65    account = "copy paste account of signer here"
66    ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log)
67
68    # using web3
69    web3.personal.sign(hash, web3.eth.defaultAccount, console.log)
70
71    Signature will be different for different accounts
72    0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
73    */
74    pub fn get_eth_signed_message_hash(&self, message_hash: FixedBytes<32>) -> FixedBytes<32> {
75        let message_to_be_decoded = [SIGNED_MESSAGE_HEAD.as_bytes(), &message_hash.to_vec()].concat();
76        keccak(message_to_be_decoded).into()
77    }
78
79    /* 4. Verify signature
80    signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
81    to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
82    amount = 123
83    message = "coffee and donuts"
84    nonce = 1
85    signature =
86        0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
87    */
88    pub fn verify(
89        &self,
90        signer: Address,
91        to: Address,
92        amount: U256,
93        message: String,
94        nonce: U256,
95        signature: Bytes,
96    ) -> Result<bool, VerifySignatureError> {
97        let message_hash = self.get_message_hash(to, amount, message, nonce);
98        let eth_signed_message_hash = self.get_eth_signed_message_hash(message_hash);
99        match self.recover_signer(eth_signed_message_hash, signature) {
100            Ok(recovered_signer) => Ok(recovered_signer == signer),
101            Err(err) => Err(err),
102        }
103    }
104
105    pub fn recover_signer(
106        &self,
107        eth_signed_message_hash: FixedBytes<32>,
108        signature: Bytes
109    ) -> Result<Address, VerifySignatureError> {
110        let (r, s, v) = self.split_signature(signature);
111        self.ecrecover_call(eth_signed_message_hash, v, r, s)
112    }
113
114    /// Invoke the ECRECOVER precompile.
115    pub fn ecrecover_call(
116        &self,
117        hash: FixedBytes<32>,
118        v: u8,
119        r: FixedBytes<32>,
120        s: FixedBytes<32>,
121    ) -> Result<Address, VerifySignatureError> {
122        let data = (hash, v, r, s);
123        let encoded_data = ECRECOVERType::abi_encode(&data);
124        match call::static_call(Call::new(), ECRECOVER, &encoded_data) {
125            Ok(result) => Ok(SOLAddress::abi_decode(&result, false).unwrap()),
126            Err(_) => Err(VerifySignatureError::EcrecoverCallError(EcrecoverCallError{})),
127        }
128    }
129
130
131    pub fn split_signature(
132        &self,
133        signature: Bytes
134    ) -> (FixedBytes<32>, FixedBytes<32>, u8) {
135        let r = FixedBytes::from_slice(&signature[0..32]);
136        let s = FixedBytes::from_slice(&signature[32..64]);
137        let v = signature[64];
138        (r, s, v)
139    }
140            
141}

Cargo.toml

1[package]
2name = "stylus-verify-signature"
3version = "0.1.7"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
7
8[dependencies]
9alloy-primitives = "=0.7.6"
10alloy-sol-types = "=0.7.6"
11mini-alloc = "0.4.2"
12stylus-sdk = "0.6.0"
13hex = "0.4.3"
14sha3 = "0.10.8"
15
16[dev-dependencies]
17tokio = { version = "1.12.0", features = ["full"] }
18ethers = "2.0"
19eyre = "0.6.8"
20
21[features]
22export-abi = ["stylus-sdk/export-abi"]
23
24[lib]
25crate-type = ["lib", "cdylib"]
26
27[profile.release]
28codegen-units = 1
29strip = true
30lto = true
31panic = "abort"
32opt-level = "s"
1[package]
2name = "stylus-verify-signature"
3version = "0.1.7"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
7
8[dependencies]
9alloy-primitives = "=0.7.6"
10alloy-sol-types = "=0.7.6"
11mini-alloc = "0.4.2"
12stylus-sdk = "0.6.0"
13hex = "0.4.3"
14sha3 = "0.10.8"
15
16[dev-dependencies]
17tokio = { version = "1.12.0", features = ["full"] }
18ethers = "2.0"
19eyre = "0.6.8"
20
21[features]
22export-abi = ["stylus-sdk/export-abi"]
23
24[lib]
25crate-type = ["lib", "cdylib"]
26
27[profile.release]
28codegen-units = 1
29strip = true
30lto = true
31panic = "abort"
32opt-level = "s"