Skip to content

Commit

Permalink
Add secp256k1 and keccak256 host functions (#839)
Browse files Browse the repository at this point in the history
  • Loading branch information
graydon committed Jun 16, 2023
1 parent e64000d commit 28d67db
Show file tree
Hide file tree
Showing 42 changed files with 1,307 additions and 249 deletions.
300 changes: 282 additions & 18 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Expand Up @@ -27,7 +27,7 @@ soroban-native-sdk-macros = { version = "0.0.16", path = "soroban-native-sdk-mac
[workspace.dependencies.stellar-xdr]
version = "0.0.16"
git = "https://github.com/stellar/rs-stellar-xdr"
rev = "60ea4cda01f1deba2ae7db5657e502e8f4381826"
rev = "f7b43ea2a4a36a87ebde2cb6050ea4bd7540df21"
default-features = false

[workspace.dependencies.wasmi]
Expand Down
8 changes: 8 additions & 0 deletions deny.toml
Expand Up @@ -265,6 +265,14 @@ skip = [
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
# while we're waiting for dalek to go to 2.0 we have
# duplicates of a whole lot of ECC dependencies due
# to k256
{ name = "block-buffer", version = "=0.9.0" },
{ name = "sha2", version = "=0.9.9" },
{ name = "signature", version = "=1.6.4" },
{ name = "rand_core", version = "=0.5.1"},
{ name = "digest", version = "=0.9.0" }
]

# This section is considered when running `cargo deny check sources`.
Expand Down
32 changes: 32 additions & 0 deletions soroban-env-common/env.json
Expand Up @@ -1548,6 +1548,38 @@
}
],
"return": "Void"
},
{
"export": "1",
"name": "compute_hash_keccak256",
"args": [
{
"name": "x",
"type": "BytesObject"
}
],
"return": "BytesObject",
"docs": "Returns the keccak256 hash of given input bytes."
},
{
"export": "2",
"name": "recover_key_ecdsa_secp256k1",
"args": [
{
"name": "msg_digest",
"type": "BytesObject"
},
{
"name": "signature",
"type": "BytesObject"
},
{
"name": "recovery_id",
"type": "U32Val"
}
],
"return": "BytesObject",
"docs": "Recovers the SEC-1-encoded ECDSA secp256k1 public key that produced a given 64-byte signature over a given 32-byte message digest, for a given recovery_id byte."
}
]
},
Expand Down
2 changes: 1 addition & 1 deletion soroban-env-common/src/meta.rs
Expand Up @@ -42,7 +42,7 @@ pub const ENV_META_V0_SECTION_NAME: &str = "contractenvmetav0";

soroban_env_macros::generate_env_meta_consts!(
ledger_protocol_version: 20,
pre_release_version: 43,
pre_release_version: 44,
);

pub fn get_ledger_protocol_version(interface_version: u64) -> u32 {
Expand Down
6 changes: 6 additions & 0 deletions soroban-env-host/Cargo.toml
Expand Up @@ -30,6 +30,12 @@ num-integer = "0.1.45"
num-derive = "0.3.3"
log = "0.4.17"
backtrace = "0.3"
k256 = {version = "0.13.1", features=["ecdsa", "arithmetic"]}
# NB: getrandom is a transitive dependency of k256 which we're not using directly
# but we have to specify it here in order to enable its 'js' feature which
# is needed to build the host for wasm (a rare but supported config).
getrandom = { version = "0.2", features=["js"] }
sha3 = "0.10.8"

[dev-dependencies]
env_logger = "0.9.0"
Expand Down
@@ -0,0 +1,26 @@
use crate::common::HostCostMeasurement;
use k256::{PublicKey, SecretKey};
use rand::rngs::StdRng;
use soroban_env_host::{cost_runner::ComputeEcdsaSecp256k1PubKeyRun, Host};

// This measures the costs to turn one byte buffer into an EcdsaSecp256k1
// pubkey, which should be constant time. The input value is ignored.
pub(crate) struct ComputeEcdsaSecp256k1PubKeyMeasure {
key: Vec<u8>,
}

impl HostCostMeasurement for ComputeEcdsaSecp256k1PubKeyMeasure {
type Runner = ComputeEcdsaSecp256k1PubKeyRun;

fn new_random_case(_host: &Host, _rng: &mut StdRng, _input: u64) -> Vec<u8> {
// Very awkward: the 'rand' crate has two copies linked in due to
// divergence between the requirements of k256 and ed25519. The StdRng
// we're getting here is not the one k256 wants. So we use an OsRng
// here, from the package k256 wants (and re-exports).
let mut rng = k256::elliptic_curve::rand_core::OsRng;

let secret = SecretKey::random(&mut rng);
let public: PublicKey = secret.public_key();
public.to_sec1_bytes().into_vec()
}
}
@@ -0,0 +1,32 @@
use crate::common::HostCostMeasurement;
use k256::{
ecdsa::{signature::Signer, Signature, SigningKey},
SecretKey,
};
use rand::rngs::StdRng;
use soroban_env_host::{cost_runner::ComputeEcdsaSecp256k1SigRun, Host};

// This measures the costs to turn one byte buffer into an EcdsaSecp256k1
// signature, which should be constant time. The input value is ignored.
pub(crate) struct ComputeEcdsaSecp256k1SigMeasure {
sig: Vec<u8>,
}

impl HostCostMeasurement for ComputeEcdsaSecp256k1SigMeasure {
type Runner = ComputeEcdsaSecp256k1SigRun;

fn new_random_case(_host: &Host, _rng: &mut StdRng, input: u64) -> Vec<u8> {
let size = 1 + input * Self::STEP_SIZE;

// Very awkward: the 'rand' crate has two copies linked in due to
// divergence between the requirements of k256 and ed25519. The StdRng
// we're getting here is not the one k256 wants. So we use an OsRng
// here, from the package k256 wants (and re-exports).
let mut rng = k256::elliptic_curve::rand_core::OsRng;

let sec: SecretKey = SecretKey::random(&mut rng);
let msg: Vec<u8> = (0..size).map(|x| x as u8).collect();
let sig: Signature = SigningKey::from(sec).try_sign(msg.as_slice()).unwrap();
sig.to_bytes().to_vec()
}
}
@@ -0,0 +1,19 @@
use crate::common::HostCostMeasurement;
use rand::rngs::StdRng;
use soroban_env_host::{cost_runner::ComputeKeccak256HashRun, Host};

// This measures the costs of performing a keccak256 hash on a variable-sized
// byte buffer. The input value is the size of the buffer. It should be
// linear time.
pub(crate) struct ComputeKeccak256HashMeasure;

impl HostCostMeasurement for ComputeKeccak256HashMeasure {
type Runner = ComputeKeccak256HashRun;

const STEP_SIZE: u64 = 100;

fn new_random_case(_host: &Host, _rng: &mut StdRng, input: u64) -> Vec<u8> {
let size = 1 + input * Self::STEP_SIZE;
(0..size).map(|n| n as u8).collect()
}
}
8 changes: 8 additions & 0 deletions soroban-env-host/benches/common/cost_types/mod.rs
@@ -1,12 +1,16 @@
mod charge_budget;
mod compute_ecdsa_secp256k1_pubkey;
mod compute_ecdsa_secp256k1_sig;
mod compute_ed25519_pubkey;
mod compute_keccak256_hash;
mod compute_sha256_hash;
mod guard_frame;
mod host_mem_alloc;
mod host_mem_cmp;
mod host_mem_cpy;
mod invoke;
mod map_ops;
mod recover_ecdsa_secp256k1_key;
mod val_deser;
mod val_ser;
mod val_xdr_conv;
Expand All @@ -17,14 +21,18 @@ mod vm_ops;
mod wasm_insn_exec;

pub(crate) use charge_budget::*;
pub(crate) use compute_ecdsa_secp256k1_pubkey::*;
pub(crate) use compute_ecdsa_secp256k1_sig::*;
pub(crate) use compute_ed25519_pubkey::*;
pub(crate) use compute_keccak256_hash::*;
pub(crate) use compute_sha256_hash::*;
pub(crate) use guard_frame::*;
pub(crate) use host_mem_alloc::*;
pub(crate) use host_mem_cmp::*;
pub(crate) use host_mem_cpy::*;
pub(crate) use invoke::*;
pub(crate) use map_ops::*;
pub(crate) use recover_ecdsa_secp256k1_key::*;
pub(crate) use val_deser::*;
pub(crate) use val_ser::*;
pub(crate) use val_xdr_conv::*;
Expand Down
@@ -0,0 +1,41 @@
use crate::common::HostCostMeasurement;
use k256::{ecdsa::SigningKey, SecretKey};
use rand::rngs::StdRng;
use sha3::{Digest, Keccak256};
use soroban_env_host::{
cost_runner::{RecoverEcdsaSecp256k1KeyRun, RecoverEcdsaSecp256k1KeySample},
xdr::Hash,
Host,
};

pub(crate) struct RecoverEcdsaSecp256k1KeyMeasure;

// This measures the cost of verifying an EcdsaSecp256k1 signature of varying-length
// messages. The input value is the length of the signed message. It should cost
// linear CPU (for hashing) and zero heap memory.
impl HostCostMeasurement for RecoverEcdsaSecp256k1KeyMeasure {
type Runner = RecoverEcdsaSecp256k1KeyRun;

const STEP_SIZE: u64 = 1000;

fn new_random_case(
_host: &Host,
_rng: &mut StdRng,
input: u64,
) -> RecoverEcdsaSecp256k1KeySample {
// Very awkward: the 'rand' crate has two copies linked in due to
// divergence between the requirements of k256 and ed25519. The StdRng
// we're getting here is not the one k256 wants. So we use an OsRng
// here, from the package k256 wants (and re-exports).
let mut rng = k256::elliptic_curve::rand_core::OsRng;

let size = 1 + input * Self::STEP_SIZE;
let sec: SecretKey = SecretKey::random(&mut rng);
let msg: Vec<u8> = (0..size).map(|x| x as u8).collect();
let hash: Hash = Hash(Keccak256::digest(msg).into());
let (sig, rid) = SigningKey::from(sec)
.sign_prehash_recoverable(hash.as_slice())
.unwrap();
RecoverEcdsaSecp256k1KeySample { hash, sig, rid }
}
}
4 changes: 4 additions & 0 deletions soroban-env-host/benches/common/mod.rs
Expand Up @@ -62,8 +62,12 @@ pub(crate) fn for_each_host_cost_measurement<B: Benchmark>(
) -> std::io::Result<BTreeMap<ContractCostType, (FPCostModel, FPCostModel)>> {
let mut params: BTreeMap<ContractCostType, (FPCostModel, FPCostModel)> = BTreeMap::new();

call_bench::<B, ComputeEcdsaSecp256k1PubKeyMeasure>(&mut params)?;
call_bench::<B, ComputeEcdsaSecp256k1SigMeasure>(&mut params)?;
call_bench::<B, ComputeEd25519PubKeyMeasure>(&mut params)?;
call_bench::<B, ComputeKeccak256HashMeasure>(&mut params)?;
call_bench::<B, ComputeSha256HashMeasure>(&mut params)?;
call_bench::<B, RecoverEcdsaSecp256k1KeyMeasure>(&mut params)?;
call_bench::<B, VerifyEd25519SigMeasure>(&mut params)?;
call_bench::<B, VmInstantiationMeasure>(&mut params)?;
call_bench::<B, VmMemReadMeasure>(&mut params)?;
Expand Down
39 changes: 15 additions & 24 deletions soroban-env-host/src/budget.rs
Expand Up @@ -304,16 +304,15 @@ impl BudgetImpl {
ContractCostType::MapEntry => (),
ContractCostType::VecEntry => (),
ContractCostType::GuardFrame => (),
ContractCostType::VerifyEd25519Sig => self.tracker[i].1 = Some(0), // length of the signature buffer
ContractCostType::VerifyEd25519Sig => self.tracker[i].1 = Some(0), // length of the signed message
ContractCostType::VmMemRead => self.tracker[i].1 = Some(0), // number of bytes in the linear memory to read
ContractCostType::VmMemWrite => self.tracker[i].1 = Some(0), // number of bytes in the linear memory to write
ContractCostType::VmInstantiation => self.tracker[i].1 = Some(0), // length of the wasm bytes,
ContractCostType::InvokeVmFunction => (),
ContractCostType::ChargeBudget => (),
ContractCostType::ComputeKeccak256Hash => (),
ContractCostType::ComputeKeccak256Hash => self.tracker[i].1 = Some(0), // number of bytes in the buffer
ContractCostType::ComputeEcdsaSecp256k1Key => (),
ContractCostType::ComputeEcdsaSecp256k1Sig => (),
ContractCostType::VerifyEcdsaSecp256k1Sig => (),
ContractCostType::RecoverEcdsaSecp256k1Key => (),
}
}
Expand Down Expand Up @@ -810,23 +809,19 @@ impl Default for BudgetImpl {
cpu.linear_term = 0;
}
ContractCostType::ComputeKeccak256Hash => {
cpu.const_term = 0;
cpu.linear_term = 0;
cpu.const_term = 3322;
cpu.linear_term = 46;
}
ContractCostType::ComputeEcdsaSecp256k1Key => {
cpu.const_term = 0;
cpu.const_term = 56525;
cpu.linear_term = 0;
}
ContractCostType::ComputeEcdsaSecp256k1Sig => {
cpu.const_term = 0;
cpu.linear_term = 0;
}
ContractCostType::VerifyEcdsaSecp256k1Sig => {
cpu.const_term = 0;
cpu.const_term = 250;
cpu.linear_term = 0;
}
ContractCostType::RecoverEcdsaSecp256k1Key => {
cpu.const_term = 0;
cpu.const_term = 2319640;
cpu.linear_term = 0;
}
}
Expand Down Expand Up @@ -923,24 +918,20 @@ impl Default for BudgetImpl {
mem.linear_term = 0;
}
ContractCostType::ComputeKeccak256Hash => {
cpu.const_term = 0;
cpu.linear_term = 0;
mem.const_term = 40;
mem.linear_term = 0;
}
ContractCostType::ComputeEcdsaSecp256k1Key => {
cpu.const_term = 0;
cpu.linear_term = 0;
mem.const_term = 0;
mem.linear_term = 0;
}
ContractCostType::ComputeEcdsaSecp256k1Sig => {
cpu.const_term = 0;
cpu.linear_term = 0;
}
ContractCostType::VerifyEcdsaSecp256k1Sig => {
cpu.const_term = 0;
cpu.linear_term = 0;
mem.const_term = 0;
mem.linear_term = 0;
}
ContractCostType::RecoverEcdsaSecp256k1Key => {
cpu.const_term = 0;
cpu.linear_term = 0;
mem.const_term = 181;
mem.linear_term = 0;
}
}

Expand Down
@@ -0,0 +1,32 @@
use std::hint::black_box;

use k256::PublicKey;

use crate::{cost_runner::CostRunner, xdr::ContractCostType};

pub struct ComputeEcdsaSecp256k1PubKeyRun;

impl CostRunner for ComputeEcdsaSecp256k1PubKeyRun {
const COST_TYPE: ContractCostType = ContractCostType::ComputeEcdsaSecp256k1Key;

type SampleType = Vec<u8>;

type RecycledType = (Option<PublicKey>, Vec<u8>);

fn run_iter(host: &crate::Host, _iter: u64, sample: Self::SampleType) -> Self::RecycledType {
let pk = black_box(
host.secp256k1_pub_key_from_bytes(sample.as_slice())
.expect("ecdsa secp256k1 publickey"),
);
(Some(pk), sample)
}

fn run_baseline_iter(
host: &crate::Host,
_iter: u64,
sample: Self::SampleType,
) -> Self::RecycledType {
black_box(host.charge_budget(Self::COST_TYPE, None).unwrap());
black_box((None, sample))
}
}

0 comments on commit 28d67db

Please sign in to comment.