-
Notifications
You must be signed in to change notification settings - Fork 756
Feat/ed25519-verify #7196
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
base: pox-wf-integration
Are you sure you want to change the base?
Feat/ed25519-verify #7196
Changes from all commits
c083718
db70f53
509333c
a7f4dd9
e8dc1d2
38302e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Added ed25519-verify to clarity6 |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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)", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| 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.", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}", | ||||||
|
|
@@ -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), | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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}; | ||
|
|
@@ -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)?; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)?; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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())) | ||
| } | ||
| 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 | ||
|
|
@@ -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}; | ||
|
|
@@ -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] | ||
|
|
@@ -1061,4 +1122,35 @@ proptest! { | |
|
|
||
| prop_assert_eq!(Value::Bool(false), result); | ||
| } | ||
|
|
||
| #[tag(t_prop)] | ||
| #[test] | ||
| fn prop_ed25519_verify_accepts_valid_signatures( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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).