Skip to content

Commit 5535017

Browse files
committed
Implement ecrecover
1 parent 4f46002 commit 5535017

File tree

7 files changed

+363
-8
lines changed

7 files changed

+363
-8
lines changed

integration/stylus/ecrecover.sol

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
// smoelius: The hash and signature were generated with the default private and public key at:
5+
// https://8gwifi.org/ecsignverify.jsp
6+
//
7+
// The message was the four byte string "1234".
8+
//
9+
// For completeness, here are the private and public keys:
10+
//
11+
// -----BEGIN EC PRIVATE KEY-----
12+
// MHQCAQEEIJF1nrba+yqP4mT1I1B3ov1/Mx2ZGT7hLJJaeutk6+lxoAcGBSuBBAAK
13+
// oUQDQgAEQb1xJnh9HhX7pYbh9pwAVcVEPlhHRwxkAnI8db53xOV39WHzidZ3n9+o
14+
// yIfQIPWBkAbzwTcB0Ntj+Q4XrcPc5A==
15+
// -----END EC PRIVATE KEY-----
16+
//
17+
// -----BEGIN PUBLIC KEY-----
18+
// MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEQb1xJnh9HhX7pYbh9pwAVcVEPlhHRwxk
19+
// AnI8db53xOV39WHzidZ3n9+oyIfQIPWBkAbzwTcB0Ntj+Q4XrcPc5A==
20+
// -----END PUBLIC KEY-----
21+
22+
contract C {
23+
function test_ecrecover() public pure {
24+
bytes32 hash = 0x03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4;
25+
bytes32 r = 0x46a24eba93079bc1442a5f0d6aa74025305faf5e04f58f35ae4961b5b268aef5;
26+
bytes32 s = 0x23bf6d6a32fd70b66e1ff51edfd511cc018e8e7f4c8b3ddd4082c597b458a73c;
27+
bytes1 v = 27;
28+
29+
address actual = ecrecover(hash, v, r, s);
30+
31+
print("actual = {}".format(actual));
32+
33+
assert(address(0x6913bA9D8b921aa6702ecB2AEE31Bf41747d84f9) == actual);
34+
}
35+
}

src/codegen/expression.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2319,12 +2319,37 @@ fn expr_builtin(
23192319

23202320
// Polkadot: call ecdsa_recover(): https://docs.rs/pallet-contracts/latest/pallet_contracts/api_doc/trait.Version0.html#tymethod.ecdsa_recover
23212321
// Solana: see how neon implements this
2322-
cfg.add(vartab, Instr::Unimplemented { reachable: true });
2322+
let var_temp = vartab.temp(
2323+
&pt::Identifier {
2324+
name: "signer".to_owned(),
2325+
loc: *loc,
2326+
},
2327+
&Type::Address(false),
2328+
);
23232329

2324-
Expression::NumberLiteral {
2325-
loc: *loc,
2326-
ty: Type::Bool,
2327-
value: 0.into(),
2330+
let [hash, v, r, s] = args else {
2331+
panic!();
2332+
};
2333+
2334+
let hash = expression(hash, cfg, contract_no, func, ns, vartab, opt);
2335+
let v = expression(v, cfg, contract_no, func, ns, vartab, opt);
2336+
let r = expression(r, cfg, contract_no, func, ns, vartab, opt);
2337+
let s = expression(s, cfg, contract_no, func, ns, vartab, opt);
2338+
2339+
cfg.add(
2340+
vartab,
2341+
Instr::Call {
2342+
res: vec![var_temp],
2343+
return_tys: vec![Type::Address(false)],
2344+
call: InternalCallTy::Builtin { ast_func_no: 0 },
2345+
args: vec![hash, v, r, s],
2346+
},
2347+
);
2348+
2349+
Expression::Variable {
2350+
loc: Loc::Codegen,
2351+
ty: Type::Address(false),
2352+
var_no: var_temp,
23282353
}
23292354
}
23302355
ast::Builtin::TypeName => {

src/emit/stylus/target.rs

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,176 @@ impl<'a> TargetRuntime<'a> for StylusTarget {
651651
args: &[BasicMetadataValueEnum<'a>],
652652
first_arg_type: Option<BasicTypeEnum>,
653653
) -> Option<BasicValueEnum<'a>> {
654-
unimplemented!()
654+
emit_context!(bin);
655+
656+
assert_eq!(5, args.len());
657+
658+
let [hash, v, r, s, res] = args else {
659+
panic!();
660+
};
661+
662+
// Construct the precompile address (0x0000000000000000000000000000000000000001)
663+
// Address is stored in big-endian, so 0x01 goes in the last byte
664+
let contract = bin
665+
.builder
666+
.build_alloca(bin.value_type(), "contract")
667+
.unwrap();
668+
// Initialize address to zero
669+
bin.builder
670+
.build_store(contract, bin.address_type().const_zero())
671+
.unwrap();
672+
// Set last byte to 0x01 by casting to byte pointer
673+
let contract_byte_ptr = bin
674+
.builder
675+
.build_pointer_cast(
676+
contract,
677+
bin.context.ptr_type(AddressSpace::default()),
678+
"contract_byte_ptr",
679+
)
680+
.unwrap();
681+
bin.builder
682+
.build_store(
683+
ptr_plus_offset(
684+
bin,
685+
contract_byte_ptr,
686+
bin.context.i32_type().const_int(19 as u64, false),
687+
),
688+
bin.context.i8_type().const_int(0x01, false),
689+
)
690+
.unwrap();
691+
692+
// Construct calldata: hash (32 bytes) + v (1 byte encoded as 32 bytes) + r (32 bytes) + s (32 bytes)
693+
// ecrecover expects big-endian format, so we need to byte-swap hash, v, r, and s
694+
let calldata = bin
695+
.builder
696+
.build_array_alloca(
697+
bin.context.i8_type(),
698+
i32_const!(32 + 32 + 32 + 32 as u64),
699+
"buffer",
700+
)
701+
.unwrap();
702+
703+
// Convert hash from little-endian to big-endian
704+
let hash_be = bin
705+
.builder
706+
.build_alloca(bin.value_type(), "hash_be")
707+
.unwrap();
708+
let hash_ptr = bin
709+
.builder
710+
.build_alloca(bin.value_type(), "hash_ptr")
711+
.unwrap();
712+
bin.builder
713+
.build_store(hash_ptr, hash.into_int_value())
714+
.unwrap();
715+
call!(
716+
"__leNtobeN",
717+
&[hash_ptr.into(), hash_be.into(), i32_const!(32).into()]
718+
);
719+
call!(
720+
"__memcpy",
721+
&[calldata.into(), hash_be.into(), i32_const!(32).into()]
722+
);
723+
724+
// Convert v from little-endian to big-endian
725+
let v_be = bin.builder.build_alloca(bin.value_type(), "v_be").unwrap();
726+
let v_ptr = bin.builder.build_alloca(bin.value_type(), "v_ptr").unwrap();
727+
bin.builder.build_store(v_ptr, v.into_int_value()).unwrap();
728+
call!(
729+
"__leNtobeN",
730+
&[v_ptr.into(), v_be.into(), i32_const!(32).into()]
731+
);
732+
call!(
733+
"__memcpy",
734+
&[
735+
ptr_plus_offset(bin, calldata, i32_const!(32)).into(),
736+
v_be.into(),
737+
i32_const!(32).into()
738+
]
739+
);
740+
741+
// Convert r from little-endian to big-endian
742+
let r_be = bin.builder.build_alloca(bin.value_type(), "r_be").unwrap();
743+
let r_ptr = bin.builder.build_alloca(bin.value_type(), "r_ptr").unwrap();
744+
bin.builder.build_store(r_ptr, r.into_int_value()).unwrap();
745+
call!(
746+
"__leNtobeN",
747+
&[r_ptr.into(), r_be.into(), i32_const!(32).into()]
748+
);
749+
call!(
750+
"__memcpy",
751+
&[
752+
ptr_plus_offset(bin, calldata, i32_const!(64)).into(),
753+
r_be.into(),
754+
i32_const!(32).into()
755+
]
756+
);
757+
758+
// Convert s from little-endian to big-endian
759+
let s_be = bin.builder.build_alloca(bin.value_type(), "s_be").unwrap();
760+
let s_ptr = bin.builder.build_alloca(bin.value_type(), "s_ptr").unwrap();
761+
bin.builder.build_store(s_ptr, s.into_int_value()).unwrap();
762+
call!(
763+
"__leNtobeN",
764+
&[s_ptr.into(), s_be.into(), i32_const!(32).into()]
765+
);
766+
call!(
767+
"__memcpy",
768+
&[
769+
ptr_plus_offset(bin, calldata, i32_const!(96)).into(),
770+
s_be.into(),
771+
i32_const!(32).into()
772+
]
773+
);
774+
775+
let calldata_len = i32_const!(32 + 32 + 32 + 32);
776+
777+
let gas = i64_const!(i32::MAX as u64);
778+
779+
let return_data_len = bin
780+
.builder
781+
.build_alloca(bin.llvm_type(&Type::Uint(32)), "return_data_len")
782+
.unwrap();
783+
784+
call!(
785+
"static_call_contract",
786+
&[
787+
contract.into(),
788+
calldata.into(),
789+
calldata_len.into(),
790+
gas.into(),
791+
return_data_len.into()
792+
],
793+
"static_call_contract"
794+
);
795+
796+
// ecrecover returns 32 bytes with the address right-aligned (last 20 bytes)
797+
let return_data_buffer = bin
798+
.builder
799+
.build_array_alloca(bin.context.i8_type(), i32_const!(32), "return_data_buffer")
800+
.unwrap();
801+
802+
call!(
803+
"read_return_data",
804+
&[
805+
return_data_buffer.into(),
806+
i32_zero!().into(),
807+
i32_const!(32).into()
808+
],
809+
"read_return_data"
810+
);
811+
812+
// Skip the first 12 bytes of padding to get the 20-byte address
813+
let address_ptr = ptr_plus_offset(bin, return_data_buffer, i32_const!(12));
814+
let signer = bin
815+
.builder
816+
.build_load(bin.address_type(), address_ptr, "signer")
817+
.unwrap();
818+
819+
bin.builder
820+
.build_store(res.into_pointer_value(), signer)
821+
.unwrap();
822+
823+
None
655824
}
656825

657826
/// Calls constructor
@@ -1545,3 +1714,28 @@ mod local {
15451714
.unwrap()
15461715
}
15471716
}
1717+
1718+
// smoelius: `dump_array` is useful for debugging.
1719+
#[allow(dead_code)]
1720+
fn dump_array<'a, T: TargetRuntime<'a>>(
1721+
target: &T,
1722+
bin: &Binary<'a>,
1723+
array: BasicValueEnum<'a>,
1724+
array_len: u64,
1725+
function: FunctionValue<'a>,
1726+
) {
1727+
emit_context!(bin);
1728+
let p = array.into_pointer_value();
1729+
for i in 0..array_len {
1730+
let x = bin
1731+
.builder
1732+
.build_load(
1733+
bin.llvm_type(&ast::Type::Uint(8)),
1734+
ptr_plus_offset(bin, p, i32_const!(i)),
1735+
"x",
1736+
)
1737+
.unwrap()
1738+
.into_int_value();
1739+
crate::emit::debug_value!(target, bin, Type::Uint(8), x, function);
1740+
}
1741+
}

src/sema/builtin.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ pub static BUILTIN_FUNCTIONS: Lazy<[Prototype; 29]> = Lazy::new(|| {
332332
Type::Bytes(32),
333333
],
334334
ret: vec![Type::Address(false)],
335-
target: vec![Target::EVM],
335+
target: vec![Target::EVM, Target::Stylus],
336336
doc: "Recover the address associated with the public key from elliptic curve signature",
337337
constant: false,
338338
},
@@ -1939,5 +1939,80 @@ impl Namespace {
19391939
cache_no: None,
19401940
import_no: None,
19411941
});
1942+
1943+
let loc = pt::Loc::Builtin;
1944+
let identifier = |name: &str| Identifier {
1945+
name: name.into(),
1946+
loc,
1947+
};
1948+
1949+
self.functions.push(Function::new(
1950+
loc,
1951+
loc,
1952+
identifier("ecrecover"),
1953+
None,
1954+
Vec::new(),
1955+
pt::FunctionTy::Function,
1956+
Some(pt::Mutability::Pure(loc)),
1957+
pt::Visibility::Public(Some(loc)),
1958+
vec![
1959+
Parameter {
1960+
loc,
1961+
id: Some(identifier("hash")),
1962+
ty: Type::Bytes(32),
1963+
ty_loc: Some(loc),
1964+
readonly: false,
1965+
indexed: false,
1966+
infinite_size: false,
1967+
recursive: false,
1968+
annotation: None,
1969+
},
1970+
Parameter {
1971+
loc,
1972+
id: Some(identifier("v")),
1973+
ty: Type::Uint(8),
1974+
ty_loc: Some(loc),
1975+
readonly: false,
1976+
indexed: false,
1977+
infinite_size: false,
1978+
recursive: false,
1979+
annotation: None,
1980+
},
1981+
Parameter {
1982+
loc,
1983+
id: Some(identifier("r")),
1984+
ty: Type::Bytes(32),
1985+
ty_loc: Some(loc),
1986+
readonly: false,
1987+
indexed: false,
1988+
infinite_size: false,
1989+
recursive: false,
1990+
annotation: None,
1991+
},
1992+
Parameter {
1993+
loc,
1994+
id: Some(identifier("s")),
1995+
ty: Type::Bytes(32),
1996+
ty_loc: Some(loc),
1997+
readonly: false,
1998+
indexed: false,
1999+
infinite_size: false,
2000+
recursive: false,
2001+
annotation: None,
2002+
},
2003+
],
2004+
vec![Parameter {
2005+
loc: pt::Loc::Builtin,
2006+
id: None,
2007+
ty: Type::Address(false),
2008+
ty_loc: None,
2009+
readonly: false,
2010+
indexed: false,
2011+
infinite_size: false,
2012+
recursive: false,
2013+
annotation: None,
2014+
}],
2015+
self,
2016+
));
19422017
}
19432018
}

src/sema/expression/literals.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ pub(crate) fn hex_number_literal(
133133
if n.starts_with("0x") && !n.chars().any(|c| c == '_') && n.len() == 42 {
134134
let address = to_hexstr_eip55(n);
135135

136-
if ns.target == Target::EVM {
136+
if ns.target == Target::EVM || ns.target == Target::Stylus {
137137
return if address == *n {
138138
let s: String = address.chars().skip(2).collect();
139139

0 commit comments

Comments
 (0)