Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/ed25519-verify.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ed25519-verify to clarity6
4 changes: 3 additions & 1 deletion clarity/src/vm/analysis/arithmetic_checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ impl ArithmeticOnlyChecker<'_> {
Err(Error::FunctionNotPermitted(function))
}
Sha512 | Sha512Trunc256 | Secp256k1Recover | Secp256k1Verify | Secp256r1Verify
| Hash160 | Sha256 | Keccak256 => Err(Error::FunctionNotPermitted(function)),
| Ed25519Verify | Hash160 | Sha256 | Keccak256 => {
Err(Error::FunctionNotPermitted(function))
}
Add | Subtract | Divide | Multiply | CmpGeq | CmpLeq | CmpLess | CmpGreater
| Modulo | Power | Sqrti | Log2 | BitwiseXor | And | Or | Not | Equals | If
| ConsSome | ConsOkay | ConsError | DefaultTo | UnwrapRet | UnwrapErrRet | IsOkay
Expand Down
1 change: 1 addition & 0 deletions clarity/src/vm/analysis/read_only_checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> {
| Secp256k1Recover
| Secp256k1Verify
| Secp256r1Verify
| Ed25519Verify
| ConsSome
| ConsOkay
| ConsError
Expand Down
3 changes: 2 additions & 1 deletion clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,8 @@ impl TypedNativeFunction {
| AllowanceWithNft
| AllowanceWithStacking
| AllowanceAll
| Secp256r1Verify => {
| Secp256r1Verify
| Ed25519Verify => {
return Err(StaticCheckErrorKind::Unreachable(
"Clarity 2+ keywords should not show up in 2.05".into(),
));
Expand Down
13 changes: 13 additions & 0 deletions clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,18 @@ fn check_secp256r1_verify(
Ok(TypeSignature::BoolType)
}

fn check_ed25519_verify(
checker: &mut TypeChecker,
args: &[SymbolicExpression],
context: &TypingContext,
) -> Result<TypeSignature, StaticCheckError> {
check_argument_count(3, args)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment from #7187: If #7179 merges before this, it could use the new functions described #7179 (comment).

checker.type_check_expects(&args[0], context, &TypeSignature::BUFFER_MAX)?;
checker.type_check_expects(&args[1], context, &TypeSignature::BUFFER_64)?;
checker.type_check_expects(&args[2], context, &TypeSignature::BUFFER_32)?;
Ok(TypeSignature::BoolType)
}

fn check_get_block_info(
checker: &mut TypeChecker,
args: &[SymbolicExpression],
Expand Down Expand Up @@ -1272,6 +1284,7 @@ impl TypedNativeFunction {
| AllowanceWithStacking
| AllowanceAll => Special(SpecialNativeFunction(&post_conditions::check_allowance_err)),
Secp256r1Verify => Special(SpecialNativeFunction(&check_secp256r1_verify)),
Ed25519Verify => Special(SpecialNativeFunction(&check_ed25519_verify)),
};

Ok(out)
Expand Down
3 changes: 3 additions & 0 deletions clarity/src/vm/costs/cost_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ define_named_enum!(ClarityCostFunction {
RestrictAssets("cost_restrict_assets"),
AsContractSafe("cost_as_contract_safe"),
Secp256r1verify("cost_secp256r1verify"),
Ed25519verify("cost_ed25519verify"),
Unimplemented("cost_unimplemented"),
});

Expand Down Expand Up @@ -339,6 +340,7 @@ pub trait CostValues {
fn cost_restrict_assets(n: u64) -> Result<ExecutionCost, VmExecutionError>;
fn cost_as_contract_safe(n: u64) -> Result<ExecutionCost, VmExecutionError>;
fn cost_secp256r1verify(n: u64) -> Result<ExecutionCost, VmExecutionError>;
fn cost_ed25519verify(n: u64) -> Result<ExecutionCost, VmExecutionError>;
}

impl ClarityCostFunction {
Expand Down Expand Up @@ -496,6 +498,7 @@ impl ClarityCostFunction {
ClarityCostFunction::RestrictAssets => C::cost_restrict_assets(n),
ClarityCostFunction::AsContractSafe => C::cost_as_contract_safe(n),
ClarityCostFunction::Secp256r1verify => C::cost_secp256r1verify(n),
ClarityCostFunction::Ed25519verify => C::cost_ed25519verify(n),
ClarityCostFunction::Unimplemented => Err(RuntimeError::NotImplemented.into()),
}
}
Expand Down
4 changes: 4 additions & 0 deletions clarity/src/vm/costs/costs_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,4 +765,8 @@ impl CostValues for Costs1 {
fn cost_secp256r1verify(n: u64) -> Result<ExecutionCost, VmExecutionError> {
Err(RuntimeError::NotImplemented.into())
}

fn cost_ed25519verify(n: u64) -> Result<ExecutionCost, VmExecutionError> {
Err(RuntimeError::NotImplemented.into())
}
}
4 changes: 4 additions & 0 deletions clarity/src/vm/costs/costs_2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,4 +765,8 @@ impl CostValues for Costs2 {
fn cost_secp256r1verify(n: u64) -> Result<ExecutionCost, VmExecutionError> {
Err(RuntimeError::NotImplemented.into())
}

fn cost_ed25519verify(n: u64) -> Result<ExecutionCost, VmExecutionError> {
Err(RuntimeError::NotImplemented.into())
}
}
4 changes: 4 additions & 0 deletions clarity/src/vm/costs/costs_2_testnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,4 +765,8 @@ impl CostValues for Costs2Testnet {
fn cost_secp256r1verify(n: u64) -> Result<ExecutionCost, VmExecutionError> {
Err(RuntimeError::NotImplemented.into())
}

fn cost_ed25519verify(n: u64) -> Result<ExecutionCost, VmExecutionError> {
Err(RuntimeError::NotImplemented.into())
}
}
4 changes: 4 additions & 0 deletions clarity/src/vm/costs/costs_3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,4 +783,8 @@ impl CostValues for Costs3 {
fn cost_secp256r1verify(n: u64) -> Result<ExecutionCost, VmExecutionError> {
Err(RuntimeError::NotImplemented.into())
}

fn cost_ed25519verify(n: u64) -> Result<ExecutionCost, VmExecutionError> {
Err(RuntimeError::NotImplemented.into())
}
}
5 changes: 5 additions & 0 deletions clarity/src/vm/costs/costs_4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use super::ExecutionCost;
/// overrides only `cost_contract_hash`.
use super::cost_functions::CostValues;
use super::costs_3::Costs3;
use crate::vm::RuntimeError;
use crate::vm::costs::cost_functions::linear;
use crate::vm::errors::VmExecutionError;

Expand Down Expand Up @@ -473,4 +474,8 @@ impl CostValues for Costs4 {
fn cost_secp256r1verify(n: u64) -> Result<ExecutionCost, VmExecutionError> {
Ok(ExecutionCost::runtime(51750))
}

fn cost_ed25519verify(n: u64) -> Result<ExecutionCost, VmExecutionError> {
Err(RuntimeError::NotImplemented.into())
}
}
15 changes: 15 additions & 0 deletions clarity/src/vm/docs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,20 @@ NIST P-256 curve (also known as secp256r1).",
0x031ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf83) ;; Returns false"
};

const ED25519VERIFY_API: SpecialAPI = SpecialAPI {
input_type: "(buff), (buff 64), (buff 32)",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
input_type: "(buff), (buff 64), (buff 32)",
input_type: "(buff 1048576), (buff 64), (buff 32)",

snippet: "ed25519-verify ${1:message} ${2:signature} ${3:public-key})",
output_type: "bool",
signature: "(ed25519-verify message signature public-key)",
description: "The `ed25519-verify` function verifies that the provided signature of the message
was signed with the private key that generated the public key.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the description of the outputs here as well.

example: "(ed25519-verify 0x68656c6c6f20776f726c64
0x7e8346b0d9ef1151608df9d436c646b9df23758b292e0df400032f2603417724a25997d81a95a8997a55252813589b9409893df1ec75249a5b6f38753232810e
0xec172b93ad5e563bf49683c1397357b1af93d4e937abda610c10ccc6112217c0) ;; Returns true
(ed25519-verify 0x00000000000000000000000000000000000000 0x7e8346b0d9ef1151608df9d436c646b9df23758b292e0df400032f2603417724a25997d81a95a8997a55252813589b9409893df1ec75249a5b6f38753232810e
0xec172b93ad5e563bf49683c1397357b1af93d4e937abda610c10ccc6112217c0) ;; Returns false"
};

const CONTRACT_CALL_API: SpecialAPI = SpecialAPI {
input_type: "ContractName, PublicFunctionName, Arg0, ...",
snippet: "contract-call? ${1:contract-principal} ${2:func} ${3:arg1}",
Expand Down Expand Up @@ -2900,6 +2914,7 @@ pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI {
AllowanceWithStacking => make_for_special(&ALLOWANCE_WITH_STACKING, function),
AllowanceAll => make_for_special(&ALLOWANCE_WITH_ALL, function),
Secp256r1Verify => make_for_special(&SECP256R1VERIFY_API, function),
Ed25519Verify => make_for_special(&ED25519VERIFY_API, function),
}
}

Expand Down
73 changes: 73 additions & 0 deletions clarity/src/vm/functions/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

use clarity_types::types::MAX_VALUE_SIZE;
use stacks_common::address::{
AddressHashMode, C32_ADDRESS_VERSION_MAINNET_SINGLESIG, C32_ADDRESS_VERSION_TESTNET_SINGLESIG,
};
use stacks_common::types::chainstate::StacksAddress;
use stacks_common::util::ed25519::ed25519_verify;
use stacks_common::util::hash;
use stacks_common::util::secp256k1::{Secp256k1PublicKey, secp256k1_recover, secp256k1_verify};
use stacks_common::util::secp256r1::{secp256r1_verify, secp256r1_verify_digest};
Expand Down Expand Up @@ -331,3 +333,74 @@ pub fn special_secp256r1_verify(

Ok(Value::Bool(verify_result.is_ok()))
}

pub fn special_ed25519_verify(
args: &[SymbolicExpression],
exec_state: &mut ExecutionState,
invoke_ctx: &InvocationContext,
context: &LocalContext,
) -> Result<Value, VmExecutionError> {
// (ed25519-verify message signature public-key)
// message: (buff MAX_VALUE_SIZE), signature: (buff 64), public-key: (buff 32)
check_argument_count(3, args)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If #7179 merges before this, it could use the new functions described #7179 (comment).


runtime_cost(ClarityCostFunction::Ed25519verify, exec_state, 0)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should pass in the message length.


let arg0 = args
.first()
.ok_or(RuntimeCheckErrorKind::IncorrectArgumentCount(0, 3))?;
let message_value = eval(arg0, exec_state, invoke_ctx, context)?;
let message = match message_value.as_ref() {
Value::Sequence(SequenceData::Buffer(BuffData { data }))
if data.len() <= MAX_VALUE_SIZE as usize =>
{
data
}
_ => {
return Err(RuntimeCheckErrorKind::TypeValueError(
Box::new(TypeSignature::BUFFER_MAX),
message_value.as_ref().to_error_string(),
)
.into());
}
};

let arg1 = args
.get(1)
.ok_or(RuntimeCheckErrorKind::IncorrectArgumentCount(1, 3))?;
let signature_value = eval(arg1, exec_state, invoke_ctx, context)?;
let signature = match signature_value.as_ref() {
Value::Sequence(SequenceData::Buffer(BuffData { data })) if data.len() <= 64 => {
if data.len() != 64 {
return Ok(Value::Bool(false));
}
data
}
_ => {
return Err(RuntimeCheckErrorKind::TypeValueError(
Box::new(TypeSignature::BUFFER_64),
signature_value.as_ref().to_error_string(),
)
.into());
}
};

let arg2 = args
.get(2)
.ok_or(RuntimeCheckErrorKind::IncorrectArgumentCount(2, 3))?;
let pubkey_value = eval(arg2, exec_state, invoke_ctx, context)?;
let pubkey = match pubkey_value.as_ref() {
Value::Sequence(SequenceData::Buffer(BuffData { data })) if data.len() == 32 => data,
_ => {
return Err(RuntimeCheckErrorKind::TypeValueError(
Box::new(TypeSignature::BUFFER_32),
pubkey_value.as_ref().to_error_string(),
)
.into());
}
};

let verify_result = ed25519_verify(message, signature, pubkey);

Ok(Value::Bool(verify_result.is_ok()))
}
4 changes: 4 additions & 0 deletions clarity/src/vm/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ define_versioned_named_enum_with_max!(NativeFunctions(ClarityVersion) {
AllowanceWithStacking("with-stacking", ClarityVersion::Clarity4, None),
AllowanceAll("with-all-assets-unsafe", ClarityVersion::Clarity4, None),
Secp256r1Verify("secp256r1-verify", ClarityVersion::Clarity4, None),
Ed25519Verify("ed25519-verify", ClarityVersion::Clarity6, None),
});

///
Expand Down Expand Up @@ -576,6 +577,9 @@ pub fn lookup_reserved_functions(name: &str, version: &ClarityVersion) -> Option
Secp256r1Verify => {
SpecialFunction("native_secp256r1-verify", &crypto::special_secp256r1_verify)
}
Ed25519Verify => {
SpecialFunction("native_ed25519-verify", &crypto::special_ed25519_verify)
}
};
Some(callable)
} else {
Expand Down
92 changes: 92 additions & 0 deletions clarity/src/vm/tests/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use clarity_types::types::MAX_VALUE_SIZE;
// Copyright (C) 2026 Stacks Open Internet Foundation
//
// This program is free software: you can redistribute it and/or modify
Expand All @@ -13,9 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use pinny::tag;
use proptest::collection::vec;
use proptest::prelude::*;
use stacks_common::types::chainstate::{StacksPrivateKey, StacksPublicKey};
use stacks_common::types::{PrivateKey, StacksEpochId};
use stacks_common::util::ed25519::{self, Ed25519PrivateKey, Ed25519PublicKey, MessageSignature};
use stacks_common::util::hash::{Sha256Sum, to_hex};
use stacks_common::util::secp256k1::MessageSignature as Secp256k1Signature;
use stacks_common::util::secp256r1::{Secp256r1PrivateKey, Secp256r1PublicKey};
Expand Down Expand Up @@ -736,6 +739,64 @@ fn test_secp256k1_recover_invalid_signature_returns_err_code() {
}
}

#[test]
fn test_ed25519_verify_valid_signature_returns_true() {
let sk = Ed25519PrivateKey::random();
let pk = Ed25519PublicKey::from_private(&sk);

let message = b"Hello World";

let signature = sk.sign(message).unwrap();

let program = format!(
"(ed25519-verify {} {} {})",
buff_literal(message),
buff_literal(&signature.to_bytes()),
buff_literal(&pk.to_bytes())
);

assert_eq!(
Value::Bool(true),
execute_with_parameters(
program.as_str(),
ClarityVersion::latest(),
StacksEpochId::latest(),
false
)
.expect("execution should succeed")
.expect("should return a value")
);
}

#[test]
fn test_ed25519_verify_invalid_signature_returns_false() {
let sk = Ed25519PrivateKey::random();
let pk = Ed25519PublicKey::from_private(&sk);

let message = b"Hello World";

let signature = MessageSignature::empty();

let program = format!(
"(ed25519-verify {} {} {})",
buff_literal(message),
buff_literal(&signature.to_bytes()),
buff_literal(&pk.to_bytes())
);

assert_eq!(
Value::Bool(false),
execute_with_parameters(
program.as_str(),
ClarityVersion::latest(),
StacksEpochId::latest(),
false
)
.expect("execution should succeed")
.expect("should return a value")
);
}

proptest! {
#[tag(t_prop)]
#[test]
Expand Down Expand Up @@ -1061,4 +1122,35 @@ proptest! {

prop_assert_eq!(Value::Bool(false), result);
}

#[tag(t_prop)]
#[test]
fn prop_ed25519_verify_accepts_valid_signatures(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add proptests where the message/signature/pubkey is tampered with and verify that it is always caught? Also after changing to use the strict verification, test the non-strict cases.

seed in any::<[u8; 32]>(),
message_bytes in vec(any::<u8>(), 0..MAX_VALUE_SIZE as usize)
) {
let privk = Ed25519PrivateKey::from_seed(&seed);
let pubk = Ed25519PublicKey::from_private(&privk);
let pubkey_bytes = pubk.to_bytes();

let signature: ed25519::MessageSignature = privk.sign(&message_bytes).expect("ed25519 signing should succeed");
let signature_bytes = signature.to_bytes();
let program = format!(
"(ed25519-verify {} {} {})",
buff_literal(&message_bytes),
buff_literal(&signature_bytes),
buff_literal(&pubkey_bytes)
);

let result = execute_with_parameters(
program.as_str(),
ClarityVersion::latest(),
StacksEpochId::latest(),
false,
)
.expect("execution should succeed")
.expect("should return a value");

prop_assert_eq!(Value::Bool(true), result);
}
}
Loading
Loading