Skip to content

Commit

Permalink
crypto: Add decompress_to_pubkey native function, Move example contra…
Browse files Browse the repository at this point in the history
…ct and tests for ecrecover to address (MystenLabs#4543)
  • Loading branch information
joyqvq authored Sep 12, 2022
1 parent dced2e7 commit 8092c73
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 13 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ expression: common_costs
"storage_rebate": 15
},
"SplitCoin": {
"computation_cost": 575,
"computation_cost": 576,
"storage_cost": 80,
"storage_rebate": 0
},
Expand Down
6 changes: 6 additions & 0 deletions crates/sui-framework/sources/crypto.move
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ module sui::crypto {
/// applied to Secp256k1 signatures.
public native fun ecrecover(signature: vector<u8>, hashed_msg: vector<u8>): vector<u8>;

/// @param pubkey: A 33-bytes compressed public key, a prefix either 0x02 or 0x03 and a 256-bit integer.
///
/// If the compressed public key is valid, return the 65-bytes uncompressed public key,
/// otherwise throw error.
public native fun decompress_pubkey(pubkey: vector<u8>): vector<u8>;

/// @param data: arbitrary bytes data to hash
/// Hash the input bytes using keccak256 and returns 32 bytes.
public native fun keccak256(data: vector<u8>): vector<u8>;
Expand Down
57 changes: 48 additions & 9 deletions crates/sui-framework/src/natives/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
use crate::{legacy_emit_cost, legacy_empty_cost};
use curve25519_dalek_ng::scalar::Scalar;
use fastcrypto::{
bls12381::{BLS12381PublicKey, BLS12381Signature},
Expand All @@ -16,15 +17,15 @@ use move_vm_types::{
};
use smallvec::smallvec;
use std::collections::VecDeque;

use crate::{legacy_emit_cost, legacy_empty_cost};
use sui_types::error::SuiError;

pub const FAIL_TO_RECOVER_PUBKEY: u64 = 0;
pub const INVALID_SIGNATURE: u64 = 1;
pub const INVALID_BULLETPROOF: u64 = 2;
pub const INVALID_RISTRETTO_GROUP_ELEMENT: u64 = 3;
pub const INVALID_RISTRETTO_SCALAR: u64 = 4;
pub const BULLETPROOFS_VERIFICATION_FAILED: u64 = 5;
pub const INVALID_PUBKEY: u64 = 6;

pub const BP_DOMAIN: &[u8] = b"mizu";

Expand All @@ -41,15 +42,53 @@ pub fn ecrecover(
let signature = pop_arg!(args, Vec<u8>);
// TODO: implement native gas cost estimation https://github.com/MystenLabs/sui/issues/3593
let cost = legacy_empty_cost();
match recover_pubkey(signature, hashed_msg) {
Ok(pubkey) => Ok(NativeResult::ok(
cost,
smallvec![Value::vector_u8(pubkey.as_bytes().to_vec())],
)),
Err(SuiError::InvalidSignature { error: _ }) => {
Ok(NativeResult::err(cost, INVALID_SIGNATURE))
}
Err(_) => Ok(NativeResult::err(cost, FAIL_TO_RECOVER_PUBKEY)),
}
}

fn recover_pubkey(signature: Vec<u8>, hashed_msg: Vec<u8>) -> Result<Secp256k1PublicKey, SuiError> {
match <Secp256k1Signature as ToFromBytes>::from_bytes(&signature) {
Ok(signature) => match signature.recover(&hashed_msg) {
Ok(pubkey) => Ok(NativeResult::ok(
cost,
smallvec![Value::vector_u8(pubkey.as_bytes().to_vec())],
)),
Err(_) => Ok(NativeResult::err(cost, FAIL_TO_RECOVER_PUBKEY)),
Ok(pubkey) => Ok(pubkey),
Err(e) => Err(SuiError::KeyConversionError(e.to_string())),
},
Err(_) => Ok(NativeResult::err(cost, INVALID_SIGNATURE)),
Err(e) => Err(SuiError::InvalidSignature {
error: e.to_string(),
}),
}
}

/// Convert a compressed 33-bytes Secp256k1 pubkey to an 65-bytes uncompressed one.
pub fn decompress_pubkey(
_context: &mut NativeContext,
ty_args: Vec<Type>,
mut args: VecDeque<Value>,
) -> PartialVMResult<NativeResult> {
debug_assert!(ty_args.is_empty());
debug_assert!(args.len() == 1);

let pubkey = pop_arg!(args, Vec<u8>);

// TODO: implement native gas cost estimation https://github.com/MystenLabs/sui/issues/3593
let cost = legacy_empty_cost();

match Secp256k1PublicKey::from_bytes(&pubkey) {
Ok(pubkey) => {
let uncompressed = &pubkey.pubkey.serialize_uncompressed();
Ok(NativeResult::ok(
cost,
smallvec![Value::vector_u8(uncompressed.to_vec())],
))
}
Err(_) => Ok(NativeResult::err(cost, INVALID_PUBKEY)),
}
}

Expand All @@ -68,7 +107,7 @@ pub fn keccak256(
Ok(NativeResult::ok(
cost,
smallvec![Value::vector_u8(
<sha3::Keccak256 as sha3::digest::Digest>::digest(msg)
<sha3::Keccak256 as sha3::digest::Digest>::digest(&msg)
.as_slice()
.to_vec()
)],
Expand Down
5 changes: 5 additions & 0 deletions crates/sui-framework/src/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ pub fn all_natives(
) -> NativeFunctionTable {
let sui_natives: &[(&str, &str, NativeFunction)] = &[
("crypto", "ecrecover", make_native!(crypto::ecrecover)),
(
"crypto",
"decompress_pubkey",
make_native!(crypto::decompress_pubkey),
),
("crypto", "keccak256", make_native!(crypto::keccak256)),
(
"crypto",
Expand Down
88 changes: 87 additions & 1 deletion crates/sui-framework/tests/crypto_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
module sui::crypto_tests {
use sui::crypto;
use sui::elliptic_curve as ec;
use std::vector;

#[test]
fun test_ecrecover_pubkey() {
// test case generated against https://docs.rs/secp256k1/latest/secp256k1/
Expand All @@ -16,6 +18,20 @@ module sui::crypto_tests {
assert!(pubkey == pubkey_bytes, 0);
}

#[test]
fun test_ecrecover_pubkey_2() {
// Test case from go-ethereum: https://github.com/ethereum/go-ethereum/blob/master/crypto/signature_test.go#L37
// hashed_msg: 0xce0677bb30baa8cf067c88db9811f4333d131bf8bcf12fe7065d211dce971008
// sig: 0x90f27b8b488db00b00606796d2987f6a5f59ae62ea05effe84fef5b8b0e549984a691139ad57a3f0b906637673aa2f63d1f55cb1a69199d4009eea23ceaddc9301
// pubkey: 0x02e32df42865e97135acfb65f3bae71bdc86f4d49150ad6a440b6f15878109880a
let hashed_msg = vector[206, 6, 119, 187, 48, 186, 168, 207, 6, 124, 136, 219, 152, 17, 244, 51, 61, 19, 27, 248, 188, 241, 47, 231, 6, 93, 33, 29, 206, 151, 16, 8];
let sig = vector[144, 242, 123, 139, 72, 141, 176, 11, 0, 96, 103, 150, 210, 152, 127, 106, 95, 89, 174, 98, 234, 5, 239, 254, 132, 254, 245, 184, 176, 229, 73, 152, 74, 105, 17, 57, 173, 87, 163, 240, 185, 6, 99, 118, 115, 170, 47, 99, 209, 245, 92, 177, 166, 145, 153, 212, 0, 158, 234, 35, 206, 173, 220, 147, 1];
let pubkey_bytes = vector[2, 227, 45, 244, 40, 101, 233, 113, 53, 172, 251, 101, 243, 186, 231, 27, 220, 134, 244, 212, 145, 80, 173, 106, 68, 11, 111, 21, 135, 129, 9, 136, 10];

let pubkey = crypto::ecrecover(sig, hashed_msg);
assert!(pubkey == pubkey_bytes, 0);
}

#[test]
#[expected_failure(abort_code = 0)]
fun test_ecrecover_pubkey_fail_to_recover() {
Expand Down Expand Up @@ -240,4 +256,74 @@ module sui::crypto_tests {
let verify = crypto::secp256k1_verify(sig, pk, msg);
assert!(verify == false, 0)
}
}

#[test]
fun test_ecrecover_eth_address() {
// Due to the lack of conversion tool in Move, here we convert hex to vector in python3: [x for x in bytearray.fromhex(hex_string[2:])]
// Test case from https://web3js.readthedocs.io/en/v1.7.5/web3-eth-accounts.html#recover
// signature: 0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c
// hashed_msg: 0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655
// address: 0x2c7536e3605d9c16a7a3d7b1898e529396a65c23
let sig = vector[185, 20, 103, 229, 112, 166, 70, 106, 169, 233, 135, 108, 188, 208, 19, 186, 186, 2, 144, 11, 137, 121, 212, 63, 226, 8, 164, 164, 243, 57, 245, 253, 96, 7, 231, 76, 216, 46, 3, 123, 128, 1, 134, 66, 47, 194, 218, 22, 124, 116, 126, 240, 69, 229, 209, 138, 95, 93, 67, 0, 248, 225, 160, 41, 28];
let hashed_msg = vector[29, 164, 75, 88, 110, 176, 114, 159, 247, 10, 115, 195, 38, 146, 111, 110, 213, 162, 95, 91, 5, 110, 127, 71, 251, 198, 229, 141, 134, 135, 22, 85];
let addr1 = vector[44, 117, 54, 227, 96, 93, 156, 22, 167, 163, 215, 177, 137, 142, 82, 147, 150, 166, 92, 35];
let addr = ecrecover_eth_address(sig, hashed_msg);
assert!(addr == addr1, 0);

// Test case from https://etherscan.io/verifySig/9754
// sig: 0xcb614cba67d6a37b9cb90d21635d81ed035b8ccb99f0befe05495b819111119b17ecf0c0cb4bcc781de387206f6dfcd9f1b99e1b54b44c376412d8f5c919b1981b
// hashed_msg: 0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655
// addr: 0x4cbf668fca6f10d01f161122534044436b80702e
let sig = vector[203, 97, 76, 186, 103, 214, 163, 123, 156, 185, 13, 33, 99, 93, 129, 237, 3, 91, 140, 203, 153, 240, 190, 254, 5, 73, 91, 129, 145, 17, 17, 155, 23, 236, 240, 192, 203, 75, 204, 120, 29, 227, 135, 32, 111, 109, 252, 217, 241, 185, 158, 27, 84, 180, 76, 55, 100, 18, 216, 245, 201, 25, 177, 152, 27];
let hashed_msg = vector[29, 164, 75, 88, 110, 176, 114, 159, 247, 10, 115, 195, 38, 146, 111, 110, 213, 162, 95, 91, 5, 110, 127, 71, 251, 198, 229, 141, 134, 135, 22, 85];
let addr1 = vector[76, 191, 102, 143, 202, 111, 16, 208, 31, 22, 17, 34, 83, 64, 68, 67, 107, 128, 112, 46];
let addr = ecrecover_eth_address(sig, hashed_msg);
assert!(addr == addr1, 0);

// Test case from https://goerli.etherscan.io/tx/0x18f72457b356f367db214de9dda07f5d253ebfeb5c426b0d9d5b346b4ba8d021
// sig: 0x8e809da5ca76e6371ba8dcaa748fc2973f0d9862f76ed08f55b869f5e73591dd24a7367f1ee9e6e3723d13bb0a7092fafb8851f7eecd4a8d34c977013e1551482e
// hashed_msg: 0x529283629f75203330f0acf68bdbc4e879047fe75da8071c079c495bbb9fb78a
// addr: 0x4cbf668fca6f10d01f161122534044436b80702e
let sig = vector[142, 128, 157, 165, 202, 118, 230, 55, 27, 168, 220, 170, 116, 143, 194, 151, 63, 13, 152, 98, 247, 110, 208, 143, 85, 184, 105, 245, 231, 53, 145, 221, 36, 167, 54, 127, 30, 233, 230, 227, 114, 61, 19, 187, 10, 112, 146, 250, 251, 136, 81, 247, 238, 205, 74, 141, 52, 201, 119, 1, 62, 21, 81, 72, 46];
let hashed_msg = vector[82, 146, 131, 98, 159, 117, 32, 51, 48, 240, 172, 246, 139, 219, 196, 232, 121, 4, 127, 231, 93, 168, 7, 28, 7, 156, 73, 91, 187, 159, 183, 138];
let addr1 = vector[76, 191, 102, 143, 202, 111, 16, 208, 31, 22, 17, 34, 83, 64, 68, 67, 107, 128, 112, 46];
let addr = ecrecover_eth_address(sig, hashed_msg);
assert!(addr == addr1, 0);
}

// Helper Move function to recover signature directly to an ETH address.
fun ecrecover_eth_address(sig: vector<u8>, hashed_msg: vector<u8>): vector<u8> {
// Normalize the last byte of the signature to be 0 or 1.
let v = vector::borrow_mut(&mut sig, 64);
if (*v == 27) {
*v = 0;
} else if (*v == 28) {
*v = 1;
} else if (*v > 35) {
*v = (*v - 1) % 2;
};

let pubkey = crypto::ecrecover(sig, hashed_msg);
let uncompressed = crypto::decompress_pubkey(pubkey);

// Take the last 64 bytes of the uncompressed pubkey.
let uncompressed_64 = vector::empty<u8>();
let i = 1;
while (i < 65) {
let value = vector::borrow(&uncompressed, i);
vector::push_back(&mut uncompressed_64, *value);
i = i + 1;
};

// Take the last 20 bytes of the hash of the 64-bytes uncompressed pubkey.
let hashed = crypto::keccak256(uncompressed_64);
let addr = vector::empty<u8>();
let i = 12;
while (i < 32) {
let value = vector::borrow(&hashed, i);
vector::push_back(&mut addr, *value);
i = i + 1;
};
addr
}
}
44 changes: 43 additions & 1 deletion sui_programmability/examples/math/sources/ecdsa.move
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module math::ecdsa {
use sui::object::{Self, UID};
use sui::tx_context::TxContext;
use sui::transfer;

use std::vector;
/// Event on whether the signature is verified
struct VerifiedEvent has copy, drop {
is_verified: bool,
Expand Down Expand Up @@ -41,6 +41,48 @@ module math::ecdsa {
transfer::transfer(pubkey, recipient)
}

public entry fun ecrecover_to_eth_address(signature: vector<u8>, hashed_msg: vector<u8>, recipient: address, ctx: &mut TxContext) {
// Normalize the last byte of the signature to be 0 or 1.
let v = vector::borrow_mut(&mut signature, 64);
if (*v == 27) {
*v = 0;
} else if (*v == 28) {
*v = 1;
} else if (*v > 35) {
*v = (*v - 1) % 2;
};

let pubkey = crypto::ecrecover(signature, hashed_msg);
let uncompressed = crypto::decompress_pubkey(pubkey);

// Take the last 64 bytes of the uncompressed pubkey.
let uncompressed_64 = vector::empty<u8>();
let i = 1;
while (i < 65) {
let value = vector::borrow(&uncompressed, i);
vector::push_back(&mut uncompressed_64, *value);
i = i + 1;
};

// Take the last 20 bytes of the hash of the 64-bytes uncompressed pubkey.
let hashed = crypto::keccak256(uncompressed_64);
let addr = vector::empty<u8>();
let i = 12;
while (i < 32) {
let value = vector::borrow(&hashed, i);
vector::push_back(&mut addr, *value);
i = i + 1;
};

let addr_object = Output {
id: object::new(ctx),
value: addr,
};

// Transfer an output data object holding the address to the recipient.
transfer::transfer(addr_object, recipient)
}

public entry fun secp256k1_verify(signature: vector<u8>, public_key: vector<u8>, hashed_msg: vector<u8>) {
event::emit(VerifiedEvent {is_verified: crypto::secp256k1_verify(signature, public_key, hashed_msg)});
}
Expand Down

0 comments on commit 8092c73

Please sign in to comment.