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
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}
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"