Skip to content

Commit 1d0baad

Browse files
authored
init: eip-712 support (#2385)
# Goal The goal of this PR is to allow encoding and verification of EIP-712 signatures for our custom signed payloads. Related to #2278 # Discussion - The frontend side of these changes will be implemented in #2281 - I didn't know which chain ID to use so used the test chainID for pallet revive. We need to set these once we figure out those values. # Checklist - [x] Unit Tests added? - [x] e2e Tests added? - [x] Spec version incremented?
1 parent 5b1451c commit 1d0baad

32 files changed

+1856
-78
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/primitives/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ sp-externalities = { workspace = true }
3131
sp-runtime-interface = { workspace = true }
3232
libsecp256k1 = { workspace = true, features = ["hmac"] }
3333
log = "0.4.22"
34+
lazy_static = { workspace = true }
3435

3536
[features]
3637
default = ['std']

common/primitives/src/handles.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ use scale_info::TypeInfo;
88
use serde::{Deserialize, Serialize};
99
use sp_core::ConstU32;
1010
extern crate alloc;
11-
use alloc::vec::Vec;
11+
use crate::{
12+
node::EIP712Encode, signatures::get_eip712_encoding_prefix, utils::to_abi_compatible_number,
13+
};
14+
use alloc::{boxed::Box, vec::Vec};
15+
use lazy_static::lazy_static;
16+
use sp_core::U256;
1217

1318
/// The minimum base and canonical handle (not including suffix or delimiter) length in characters
1419
pub const HANDLE_CHARS_MIN: u32 = 3;
@@ -58,6 +63,31 @@ impl<BlockNumber> ClaimHandlePayload<BlockNumber> {
5863
}
5964
}
6065

66+
impl<BlockNumber> EIP712Encode for ClaimHandlePayload<BlockNumber>
67+
where
68+
BlockNumber: Into<U256> + TryFrom<U256> + Copy,
69+
{
70+
fn encode_eip_712(&self) -> Box<[u8]> {
71+
lazy_static! {
72+
// get prefix and domain separator
73+
static ref PREFIX_DOMAIN_SEPARATOR: Box<[u8]> =
74+
get_eip712_encoding_prefix("0xcccccccccccccccccccccccccccccccccccccccc");
75+
76+
// signed payload
77+
static ref MAIN_TYPE_HASH: [u8; 32] =
78+
sp_io::hashing::keccak_256(b"ClaimHandlePayload(string handle,uint32 expiration)");
79+
}
80+
let coded_handle = sp_io::hashing::keccak_256(self.base_handle.as_ref());
81+
let expiration: U256 = self.expiration.into();
82+
let coded_expiration = to_abi_compatible_number(expiration.as_u128());
83+
let message = sp_io::hashing::keccak_256(
84+
&[MAIN_TYPE_HASH.as_slice(), &coded_handle, &coded_expiration].concat(),
85+
);
86+
let combined = [PREFIX_DOMAIN_SEPARATOR.as_ref(), &message].concat();
87+
combined.into_boxed_slice()
88+
}
89+
}
90+
6191
/// RPC Response form for a Handle
6292
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
6393
#[derive(Clone, Encode, Decode, PartialEq, Debug, TypeInfo, Eq)]

common/primitives/src/node.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,9 @@ pub trait UtilityProvider<Origin, RuntimeCall> {
6868
/// Passthrough into the Utility::batch_all call
6969
fn batch_all(origin: Origin, calls: Vec<RuntimeCall>) -> DispatchResultWithPostInfo;
7070
}
71+
72+
/// Trait that must be implemented to be able to encode the payload to eip-712 compatible signatures
73+
pub trait EIP712Encode {
74+
/// encodes the type without hashing it
75+
fn encode_eip_712(&self) -> Box<[u8]>;
76+
}

common/primitives/src/signatures.rs

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ use frame_support::{
55
__private::{codec, RuntimeDebug},
66
pallet_prelude::{Decode, Encode, MaxEncodedLen, TypeInfo},
77
};
8+
use lazy_static::lazy_static;
89
use parity_scale_codec::{alloc::string::ToString, DecodeWithMemTracking};
910
use sp_core::{
11+
bytes::from_hex,
1012
crypto,
1113
crypto::{AccountId32, FromEntropy},
1214
ecdsa, ed25519,
@@ -19,6 +21,8 @@ use sp_runtime::{
1921
MultiSignature,
2022
};
2123
extern crate alloc;
24+
use crate::{msa::H160, utils::to_abi_compatible_number};
25+
use alloc::boxed::Box;
2226

2327
/// Ethereum message prefix eip-191
2428
const ETHEREUM_MESSAGE_PREFIX: &[u8; 26] = b"\x19Ethereum Signed Message:\n";
@@ -30,6 +34,9 @@ pub trait AccountAddressMapper<AccountId> {
3034

3135
/// mapping to bytes of a public key or an address
3236
fn to_bytes32(public_key_or_address: &[u8]) -> [u8; 32];
37+
38+
/// reverses an accountId to it's 20 byte ethereum address
39+
fn to_ethereum_address(account_id: AccountId) -> H160;
3340
}
3441

3542
/// converting raw address bytes to 32 bytes Ethereum compatible addresses
@@ -79,6 +86,16 @@ impl AccountAddressMapper<AccountId32> for EthereumAddressMapper {
7986
hashed[20..].fill(0xEE);
8087
hashed
8188
}
89+
90+
fn to_ethereum_address(account_id: AccountId32) -> H160 {
91+
let mut eth_address = [0u8; 20];
92+
if account_id.as_slice()[20..] == *[0xEE; 12].as_slice() {
93+
eth_address[..].copy_from_slice(&account_id.as_slice()[0..20]);
94+
} else {
95+
log::error!("Incompatible ethereum account id is provided {:?}", account_id);
96+
}
97+
eth_address.into()
98+
}
8299
}
83100

84101
/// Signature verify that can work with any known signature types.
@@ -345,14 +362,57 @@ fn check_ethereum_signature<L: Lazy<[u8]>>(
345362
return true
346363
}
347364

348-
// signature of raw payload, compatible with polkadotJs signatures
365+
// PolkadotJs raw payload signatures
366+
// or Ethereum based EIP-712 compatible signatures
349367
let hashed = sp_io::hashing::keccak_256(msg.get());
350368
verify_signature(signature.as_ref(), &hashed, signer)
351369
}
352370

371+
/// returns the ethereum encoded prefix and domain separator for EIP-712 signatures
372+
pub fn get_eip712_encoding_prefix(verifier_contract_address: &str) -> Box<[u8]> {
373+
lazy_static! {
374+
// domain separator
375+
static ref DOMAIN_TYPE_HASH: [u8; 32] = sp_io::hashing::keccak_256(
376+
b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
377+
);
378+
379+
static ref DOMAIN_NAME: [u8; 32] = sp_io::hashing::keccak_256(b"Frequency");
380+
static ref DOMAIN_VERSION: [u8; 32] = sp_io::hashing::keccak_256(b"1");
381+
// TODO: USE correct chain ids for different networks
382+
static ref CHAIN_ID: [u8; 32] = to_abi_compatible_number(420420420u32);
383+
}
384+
let verifier_contract: [u8; 20] = from_hex(verifier_contract_address)
385+
.unwrap_or_default()
386+
.try_into()
387+
.unwrap_or_default();
388+
389+
// eip-712 prefix 0x1901
390+
let eip_712_prefix = [25, 1];
391+
392+
let mut zero_prefixed_verifier_contract = [0u8; 32];
393+
zero_prefixed_verifier_contract[12..].copy_from_slice(&verifier_contract);
394+
395+
let domain_separator = sp_io::hashing::keccak_256(
396+
&[
397+
DOMAIN_TYPE_HASH.as_slice(),
398+
DOMAIN_NAME.as_slice(),
399+
DOMAIN_VERSION.as_slice(),
400+
CHAIN_ID.as_slice(),
401+
&zero_prefixed_verifier_contract,
402+
]
403+
.concat(),
404+
);
405+
let combined = [eip_712_prefix.as_slice(), domain_separator.as_slice()].concat();
406+
combined.into_boxed_slice()
407+
}
408+
353409
#[cfg(test)]
354410
mod tests {
355-
use crate::signatures::{UnifiedSignature, UnifiedSigner};
411+
use crate::{
412+
handles::ClaimHandlePayload,
413+
node::EIP712Encode,
414+
signatures::{UnifiedSignature, UnifiedSigner},
415+
};
356416
use impl_serde::serialize::from_hex;
357417
use sp_core::{ecdsa, Pair};
358418
use sp_runtime::{
@@ -411,6 +471,29 @@ mod tests {
411471
assert!(unified_signature.verify(&payload[..], &unified_signer.into_account()));
412472
}
413473

474+
#[test]
475+
fn ethereum_eip712_signatures_for_claim_handle_payload_should_work() {
476+
let payload = ClaimHandlePayload { base_handle: b"Alice".to_vec(), expiration: 100u32 };
477+
let encoded_payload = payload.encode_eip_712();
478+
479+
// following signature is generated via Metamask using the same input to check compatibility
480+
let signature_raw = from_hex("0x832d1f6870118f5fc6e3cc314152b87dc452bd607581f16b1e39142b553260f8397e80c9f7733aecf1bd46d4e84ad333c648e387b069fa93b4b1ca4fa0fd406b1c").expect("Should convert");
481+
let unified_signature = UnifiedSignature::from(ecdsa::Signature::from_raw(
482+
signature_raw.try_into().expect("should convert"),
483+
));
484+
485+
// Non-compressed public key associated with the keypair used in Metamask
486+
// 0x509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9fa213197dc0666e85529d6c9dda579c1295d61c417f01505765481e89a4016f02
487+
let public_key = ecdsa::Public::from_raw(
488+
from_hex("0x02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f")
489+
.expect("should convert")
490+
.try_into()
491+
.expect("invalid size"),
492+
);
493+
let unified_signer = UnifiedSigner::from(public_key);
494+
assert!(unified_signature.verify(&encoded_payload[..], &unified_signer.into_account()));
495+
}
496+
414497
#[test]
415498
fn ethereum_invalid_signatures_should_fail() {
416499
let payload = from_hex("0x0a0300e659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e028c7d0a3500000000830000000100000026c1147602cf6557f4e0068a78cd4b22b6f6b03e106d05618cde8537e4ffe4548de1bcb12a1d42e58b218a7abb03cb629111625cf3449640d837c5aa98b87d8e00").expect("Should convert");
@@ -437,13 +520,15 @@ mod tests {
437520
// act
438521
let account_id = EthereumAddressMapper::to_account_id(&eth);
439522
let bytes = EthereumAddressMapper::to_bytes32(&eth);
523+
let reversed = EthereumAddressMapper::to_ethereum_address(account_id.clone());
440524

441525
// assert
442526
let expected_address =
443527
from_hex("0x1111111111111111111111111111111111111111eeeeeeeeeeeeeeeeeeeeeeee")
444528
.expect("should be hex");
445529
assert_eq!(account_id, AccountId32::new(expected_address.clone().try_into().unwrap()));
446530
assert_eq!(bytes.to_vec(), expected_address);
531+
assert_eq!(reversed.0.to_vec(), eth);
447532
}
448533

449534
#[test]

common/primitives/src/utils.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ pub fn get_chain_type_by_genesis_hash(genesis_hash: &[u8]) -> DetectedChainType
3232
_ => DetectedChainType::Unknown,
3333
}
3434
}
35+
36+
/// Generic function for converting any unsigned integer to a 32-byte array compatible with ETH abi
37+
pub fn to_abi_compatible_number<T: Into<u128>>(value: T) -> [u8; 32] {
38+
let value_u128: u128 = value.into();
39+
let bytes = value_u128.to_be_bytes();
40+
let start_idx = 32 - bytes.len();
41+
let mut result = [0u8; 32];
42+
result[start_idx..].copy_from_slice(&bytes);
43+
result
44+
}
45+
3546
/// Handle serializing and deserializing from `Vec<u8>` to hexadecimal
3647
#[cfg(feature = "std")]
3748
pub mod as_hex {
@@ -146,6 +157,7 @@ mod tests {
146157
use parity_scale_codec::{Decode, Encode};
147158
use scale_info::TypeInfo;
148159
use serde::{Deserialize, Serialize};
160+
use sp_core::U256;
149161

150162
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
151163
#[derive(Default, Clone, Encode, Decode, PartialEq, Debug, TypeInfo, Eq)]
@@ -297,4 +309,52 @@ mod tests {
297309
// assert
298310
assert_eq!(detected, DetectedChainType::FrequencyPaseoTestNet);
299311
}
312+
313+
#[test]
314+
fn abi_compatible_number_should_work_with_different_types() {
315+
// For u8
316+
let u8_val: u8 = 42;
317+
let coded_u8_val = to_abi_compatible_number(u8_val);
318+
let u8_val: U256 = u8_val.into();
319+
assert_eq!(
320+
coded_u8_val.to_vec(),
321+
sp_core::bytes::from_hex(&format!("0x{:064x}", u8_val)).unwrap()
322+
);
323+
324+
// For u16
325+
let u16_val: u16 = 12345;
326+
let coded_u16_val = to_abi_compatible_number(u16_val);
327+
let u16_val: U256 = u16_val.into();
328+
assert_eq!(
329+
coded_u16_val.to_vec(),
330+
sp_core::bytes::from_hex(&format!("0x{:064x}", u16_val)).unwrap()
331+
);
332+
333+
// For u32
334+
let u32_val: u32 = 305419896;
335+
let coded_u32_val = to_abi_compatible_number(u32_val);
336+
let u32_val: U256 = u32_val.into();
337+
assert_eq!(
338+
coded_u32_val.to_vec(),
339+
sp_core::bytes::from_hex(&format!("0x{:064x}", u32_val)).unwrap()
340+
);
341+
342+
// For u64
343+
let u64_val: u64 = 1234567890123456789;
344+
let coded_u64_val = to_abi_compatible_number(u64_val);
345+
let u64_val: U256 = u64_val.into();
346+
assert_eq!(
347+
coded_u64_val.to_vec(),
348+
sp_core::bytes::from_hex(&format!("0x{:064x}", u64_val)).unwrap()
349+
);
350+
351+
// For u128
352+
let u128_val: u128 = 340282366920938463463374607431768211455; // Max u128 value
353+
let coded_u128_val = to_abi_compatible_number(u128_val);
354+
let u128_val: U256 = u128_val.into();
355+
assert_eq!(
356+
coded_u128_val.to_vec(),
357+
sp_core::bytes::from_hex(&format!("0x{:064x}", u128_val)).unwrap()
358+
);
359+
}
300360
}

e2e/miscellaneous/balance.ethereum.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ describe('Balance transfer ethereum', function () {
1919
before(async function () {
2020
senderSr25519Keys = await createAndFundKeypair(fundingSource, 30n * DOLLARS);
2121
senderEthereumKeys = await createAndFundKeypair(fundingSource, 30n * DOLLARS, undefined, undefined, 'ethereum');
22-
ethereumKeys = createKeys('another-key-1', 'ethereum');
23-
ethereumKeys2 = createKeys('another-key-2', 'ethereum');
22+
ethereumKeys = createKeys('balance-key-1', 'ethereum');
23+
ethereumKeys2 = createKeys('balance-key-2', 'ethereum');
2424
sr25519Keys = createKeys('another-sr25519', 'sr25519');
2525
});
2626

e2e/msa/keyManagement.ethereum.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import {
77
CENTS,
88
signPayload,
99
MultiSignatureType,
10+
signEip712AddKeyData,
11+
getEthereumKeyPairFromUnifiedAddress,
1012
} from '../scaffolding/helpers';
1113
import { KeyringPair } from '@polkadot/keyring/types';
1214
import { AddKeyData, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
1315
import { u64 } from '@polkadot/types';
1416
import { Codec } from '@polkadot/types/types';
1517
import { getFundingSource } from '../scaffolding/funding';
16-
import { getUnifiedPublicKey } from '../scaffolding/ethereum';
18+
import { getUnifiedAddress, getUnifiedPublicKey } from '../scaffolding/ethereum';
1719

1820
const maxU64 = 18_446_744_073_709_551_615n;
1921
const fundingSource = getFundingSource(import.meta.url);
@@ -118,5 +120,28 @@ describe('MSA Key management Ethereum', function () {
118120
// Cleanup
119121
await assert.doesNotReject(ExtrinsicHelper.deletePublicKey(keys, getUnifiedPublicKey(thirdKey)).signAndSend());
120122
});
123+
124+
it('should allow using eip-712 signatures to add a new key', async function () {
125+
const thirdKey = createKeys('third-key', 'ethereum');
126+
const newPayload = await generateAddKeyPayload({
127+
...defaultPayload,
128+
newPublicKey: getUnifiedPublicKey(thirdKey),
129+
});
130+
131+
ownerSig = await signEip712AddKeyData(
132+
getEthereumKeyPairFromUnifiedAddress(getUnifiedAddress(secondaryKey)),
133+
newPayload
134+
);
135+
newSig = await signEip712AddKeyData(
136+
getEthereumKeyPairFromUnifiedAddress(getUnifiedAddress(thirdKey)),
137+
newPayload
138+
);
139+
const op = ExtrinsicHelper.addPublicKeyToMsa(secondaryKey, ownerSig, newSig, newPayload);
140+
const { target: event } = await op.fundAndSend(fundingSource);
141+
assert.notEqual(event, undefined, 'should have added public key via eip-712');
142+
143+
// Cleanup
144+
await assert.doesNotReject(ExtrinsicHelper.deletePublicKey(keys, getUnifiedPublicKey(thirdKey)).signAndSend());
145+
});
121146
});
122147
});

e2e/msa/msaTokens.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable mocha/no-skipped-tests */
21
import '@frequency-chain/api-augment';
32
import assert from 'assert';
43
import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';

0 commit comments

Comments
 (0)