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

Account Abstraction - Custom Validation Signature Scheme #229

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Code generated by scarb DO NOT EDIT.
version = 1

[[package]]
name = "account_abstraction"
version = "0.1.0"

[[package]]
name = "advanced_factory"
version = "0.1.0"
Expand Down
1 change: 1 addition & 0 deletions listings/advanced-concepts/account_abstraction/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target
14 changes: 14 additions & 0 deletions listings/advanced-concepts/account_abstraction/Scarb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "account_abstraction"
version = "0.1.0"
edition = "2023_11"

# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html

[dependencies]
starknet.workspace = true

[scripts]
test.workspace = true

[[target.starknet-contract]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use starknet::secp256_trait::{
Secp256PointTrait, Signature as Secp256Signature, recover_public_key, is_signature_entry_valid
};
use starknet::secp256r1::Secp256r1Point;
use starknet::secp256k1::Secp256k1Point;
use starknet::{ EthAddress, eth_signature::is_eth_signature_valid };
use core::traits::TryInto;


const SECP256R1_SIGNER_TYPE: felt252 = 'Secp256r1 Signer';
const SECP256K1_SIGNER_TYPE: felt252 = 'Secp256k1 Signer';
const SECP_256_R1_HALF: u256 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 / 2;
const SECP_256_K1_HALF: u256 = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 / 2;



#[derive(Drop, Copy, PartialEq, Serde, Default)]
enum SignerType {
#[default]
Secp256r1,
Secp256k1,
}

#[derive(Drop, Copy, Serde)]
enum SignerSignature {
Secp256r1: (Secp256r1Signer, Secp256Signature),
Secp256k1: (Secp256k1Signer, Secp256Signature),
}

#[derive(Drop, Copy, Serde)]
enum Signer {
Secp256r1: Secp256r1Signer,
Secp256k1: Secp256k1Signer,
}

#[derive(Drop, Copy, Serde, PartialEq)]
struct Secp256r1Signer {
pubkey: NonZero<u256>
}

#[derive(Drop, Copy, PartialEq)]
struct Secp256k1Signer {
pubkey_hash: EthAddress
}


// To ensure the pubkey hash is not zero
impl Secp256k1SignerSerde of Serde<Secp256k1Signer> {
#[inline(always)]
fn serialize(self: @Secp256k1Signer, ref output: Array<felt252>) {
self.pubkey_hash.serialize(ref output);
}

#[inline(always)]
fn deserialize(ref serialized: Span<felt252>) -> Option<Secp256k1Signer> {
let pubkey_hash = Serde::<EthAddress>::deserialize(ref serialized)?;
assert(pubkey_hash.address != 0, 'zero pub key hash' );
Option::Some(Secp256k1Signer { pubkey_hash })
}
}

// To check if secp256k1 and secp256r1 signatures are valid
trait Secp256SignatureTrait {
fn is_valid_signature(self: SignerSignature, hash: felt252) -> bool;
fn signer(self: SignerSignature) -> Signer;
}

impl Secp256SignatureImpl of Secp256SignatureTrait {
#[inline(always)]
fn is_valid_signature(self: SignerSignature, hash: felt252) -> bool {
match self {
SignerSignature::Secp256r1((
signer, signature
)) => is_valid_secp256r1_signature(hash.into(), signer, signature),
SignerSignature::Secp256k1((
signer, signature
)) => is_valid_secp256k1_signature(hash.into(), signer.pubkey_hash.into(), signature),
}
}

#[inline(always)]
fn signer(self: SignerSignature) -> Signer {
match self {
SignerSignature::Secp256k1((signer, _)) => Signer::Secp256k1(signer),
SignerSignature::Secp256r1((signer, _)) => Signer::Secp256r1(signer),
}
}
}

// To validate secp256k1 signature
#[inline(always)]
fn is_valid_secp256k1_signature(hash: u256, pubkey_hash: EthAddress, signature: Secp256Signature) -> bool {
assert(signature.s <= SECP_256_K1_HALF, 'malleable signature');
is_eth_signature_valid(hash, signature, pubkey_hash).is_ok()
}

// To validate secp256r1 signature
#[inline(always)]
fn is_valid_secp256r1_signature(hash: u256, signer: Secp256r1Signer, signature: Secp256Signature) -> bool {
assert(is_signature_entry_valid::<Secp256r1Point>(signature.s), 'invalid s-value');
assert(is_signature_entry_valid::<Secp256r1Point>(signature.r), 'invalid r-value');
assert(signature.s <= SECP_256_R1_HALF, 'malleable signature');
let recovered_pubkey = recover_public_key::<Secp256r1Point>(hash, signature).expect('invalid sign format');
let (recovered_signer, _) = recovered_pubkey.get_coordinates().expect('invalid sig format');
recovered_signer == signer.pubkey.into()
}

// impl to convert signer type into felt252 using into()
impl SignerTypeIntoFelt252 of Into<SignerType, felt252> {
#[inline(always)]
fn into(self: SignerType) -> felt252 {
match self {
SignerType::Secp256k1 => 1,
SignerType::Secp256r1 => 2,
}
}
}


// impl to convert u256 type into SignerType using try_into()
impl U256TryIntoSignerType of TryInto<u256, SignerType> {
#[inline(always)]
fn try_into(self: u256) -> Option<SignerType> {
if self == 1 {
Option::Some(SignerType::Secp256k1)
} else if self == 2 {
Option::Some(SignerType::Secp256r1)
} else {
Option::None
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod account_abstraction;
40 changes: 40 additions & 0 deletions src/advanced-concepts/account-abstraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Account Abstraction

### Understanding Ethereum Account System

Traditionally, there are two types of account on Ethereum: Externally Owned Accounts known as EOAs and smart contract Accounts.

EOAs are normal accounts used by individuals, they have private keys and can sign transactions. Smart contract accounts do not have private keys, therefore they cannot initiate or sign transactions. All transactions on Ethereum must be initiated by an EOA.

Ethereum accounts have many challenges such as:

i. Key Management: Users must secure and never lose their seed phrase and private keys, otherwise they risk losing access to their accounts and assets forever.

Also, once a thief gains access to your private keys or seed phrase, he gains complete access to your account and its assets.

ii. Bad User Experience: The account system used on Ethereum makes it difficult for newbies to use crypto applications as they are always complicated to use.
Lack of account recovery options also makes it unappealing to users.

iii. Lack of Flexibility: Ethereum account system restricts custom transaction validation schemes, limiting potential enhancements on access control and security.


### What is Account Abstraction?

Account Abstraction is the concept of modifying accounts and enhancing transactions on blockchain networks.
Account Abstraction replaces EOAs with a broader model where all accounts are smart contracts, each with its own unique rules and behaviors.

Account Abstraction makes it possible for innovative account management system such as custom signature types, session keys, 2 Factor Authentication (2FA), fingerprint or facial recognition account signing, ability to pay for gas using other tokens such as USDT, ability to recover accounts without seed phrase, email recovery of accounts and so on.

### The Most Important Concepts

i. **Account:** All accounts are smart contracts that can hold assets and execute transactions on Starknet protocol, these account contracts however must implement some specific methods outlined in SNIP-6.

ii. **Signers:** These are responsible for digitally signing transactions and provide the authorization needed to initiate transactions.
Digital signatures are cryptographic proofs that transactions are authorized by corresponding accounts.

In summary, Starknet accounts are normal blockchain accounts that hold assets and initiate transactions onchain, while signers provide the authorization required to ensure that transactions originating from these accounts are secure, valid and executed.

To implement custom validation method on Starknet you have to ensure that the contract contains these three methods: `is_valid_signature`, `__validate__` and `__execute__`. These are the building block for account contracts on Starknet as contained in the SNIP-6.