diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 1ba52de6..86aa3814 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -48,6 +48,7 @@ jobs: - examples/smart-contract-upgrade/contract-version2/Cargo.toml - examples/offchain-transfers/Cargo.toml - examples/credential-registry-storage-contract/Cargo.toml + - examples/account-signature-checks/Cargo.toml steps: - name: Checkout sources @@ -407,6 +408,7 @@ jobs: - examples/cis2-multi-royalties/Cargo.toml - examples/nametoken/Cargo.toml - examples/credential-registry-storage-contract/Cargo.toml + - examples/account-signature-checks/Cargo.toml features: - @@ -505,6 +507,7 @@ jobs: - examples/smart-contract-upgrade/contract-version2/Cargo.toml - examples/offchain-transfers/Cargo.toml - examples/credential-registry-storage-contract/Cargo.toml + - examples/account-signature-checks/Cargo.toml steps: - name: Checkout sources @@ -635,6 +638,7 @@ jobs: - examples/smart-contract-upgrade/contract-version2/Cargo.toml - examples/offchain-transfers/Cargo.toml - examples/credential-registry-storage-contract/Cargo.toml + - examples/account-signature-checks/Cargo.toml steps: - name: Checkout sources diff --git a/concordium-contracts-common b/concordium-contracts-common index 6915c9ba..b65a4561 160000 --- a/concordium-contracts-common +++ b/concordium-contracts-common @@ -1 +1 @@ -Subproject commit 6915c9ba42617dcfe312969c05d7505bea894d2b +Subproject commit b65a4561dee10b2668a488cbd540f62d12fd99bc diff --git a/concordium-std/CHANGELOG.md b/concordium-std/CHANGELOG.md index c66d820d..aa5dae6c 100644 --- a/concordium-std/CHANGELOG.md +++ b/concordium-std/CHANGELOG.md @@ -7,6 +7,11 @@ - Support adding `#[concordium(forward = n)]`, for enum variants, where `n` is either an unsigned integer literal, `cis2_events`, `cis3_events`, `cis4_events` or an array of the same options. Setting this attribute on a variant overrides the (de)serialization to flatten with the (de)serialization of the inner field when using derive macros such as `Serial`, `Deserial`, `DeserialWithState` and `SchemaType`. Note that setting `#[concordium(repr(u*))]` is required when using this attribute. +- Support protocol 6 smart contract extensions. In particular the `HasHost` + trait is extended with two additional host operations, `account_public_keys` + and `check_account_signature` corresponding to the two new host functions + available in protocol 6. Two new types were added to support these operations, + `AccountSignatures` and `AccountPublicKeys`. ## concordium-std 7.0.0 (2023-06-16) diff --git a/concordium-std/src/impls.rs b/concordium-std/src/impls.rs index 6445a060..5e41e850 100644 --- a/concordium-std/src/impls.rs +++ b/concordium-std/src/impls.rs @@ -1814,6 +1814,10 @@ const INVOKE_QUERY_CONTRACT_BALANCE_TAG: u32 = 3; /// Tag of the query exchange rates operation expected by the host. See /// [prims::invoke]. const INVOKE_QUERY_EXCHANGE_RATES_TAG: u32 = 4; +/// Tag of the operation to check the account's signature [prims::invoke]. +const INVOKE_CHECK_ACCOUNT_SIGNATURE_TAG: u32 = 5; +/// Tag of the query account's public keys [prims::invoke]. +const INVOKE_QUERY_ACCOUNT_PUBLIC_KEYS_TAG: u32 = 6; /// Check whether the response code from calling `invoke` is encoding a failure /// and map out the byte used for the error code. @@ -1982,6 +1986,56 @@ fn parse_query_contract_balance_response_code( } } +/// Decode the account public keys query response code. +/// +/// - Success if the last 5 bytes are all zero: +/// - the first 3 bytes encodes the return value index. +/// - In case of failure the 4th byte is used, and encodes the enviroment +/// failure where: +/// - '0x02' encodes missing account. +fn parse_query_account_public_keys_response_code( + code: u64, +) -> Result { + if let Some(error_code) = get_invoke_failure_code(code) { + if error_code == 0x02 { + Err(QueryAccountPublicKeysError) + } else { + unsafe { crate::hint::unreachable_unchecked() } + } + } else { + // Map out the 3 bytes encoding the return value index. + let return_value_index = NonZeroU32::new((code >> 40) as u32).unwrap_abort(); + Ok(ExternCallResponse::new(return_value_index)) + } +} + +/// Decode the response from checking account signatures. +/// +/// - Success if the last 5 bytes are all zero: +/// - In case of failure the 4th byte is used, and encodes the enviroment +/// failure where: +/// - '0x02' encodes missing account. +/// - '0x0a' encodes malformed data, i.e., the call was made with incorrect +/// data. +/// - '0x0b' encodes that signature validation failed. +fn parse_check_account_signature_response_code( + code: u64, +) -> Result { + if let Some(error_code) = get_invoke_failure_code(code) { + if error_code == 0x02 { + Err(CheckAccountSignatureError::MissingAccount) + } else if error_code == 0x0a { + Err(CheckAccountSignatureError::MalformedData) + } else if error_code == 0x0b { + Ok(false) + } else { + unsafe { crate::hint::unreachable_unchecked() } + } + } else { + Ok(true) + } +} + /// Decode the exchange rate response code. /// /// - Success if the last 5 bytes are all zero: @@ -2070,6 +2124,37 @@ fn query_exchange_rates_worker() -> ExchangeRates { ExchangeRates::deserial(&mut response).unwrap_abort() } +/// Helper factoring out the common behaviour of `account_public_keys` for the +/// two extern hosts below. +fn query_account_public_keys_worker(address: AccountAddress) -> QueryAccountPublicKeysResult { + let data: &[u8] = address.as_ref(); + let response = unsafe { + prims::invoke(INVOKE_QUERY_ACCOUNT_PUBLIC_KEYS_TAG, data.as_ptr() as *const u8, 32) + }; + let mut return_value = parse_query_account_public_keys_response_code(response)?; + Ok(AccountPublicKeys::deserial(&mut return_value).unwrap_abort()) +} + +fn check_account_signature_worker( + address: AccountAddress, + signatures: &AccountSignatures, + data: &[u8], +) -> CheckAccountSignatureResult { + let mut buffer = address.0.to_vec(); + signatures.serial(&mut buffer).unwrap_abort(); + (data.len() as u32).serial(&mut buffer).unwrap_abort(); + buffer.extend_from_slice(data); + + let response = unsafe { + prims::invoke( + INVOKE_CHECK_ACCOUNT_SIGNATURE_TAG, + buffer.as_ptr() as *const u8, + buffer.len() as u32, + ) + }; + parse_check_account_signature_response_code(response) +} + impl StateBuilder where S: HasStateApi, @@ -2317,6 +2402,19 @@ where parse_upgrade_response_code(response) } + fn account_public_keys(&self, address: AccountAddress) -> QueryAccountPublicKeysResult { + query_account_public_keys_worker(address) + } + + fn check_account_signature( + &self, + address: AccountAddress, + signatures: &AccountSignatures, + data: &[u8], + ) -> CheckAccountSignatureResult { + check_account_signature_worker(address, signatures, data) + } + fn state(&self) -> &S { &self.state } fn state_mut(&mut self) -> &mut S { &mut self.state } @@ -2382,6 +2480,19 @@ impl HasHost for ExternLowLevelHost { parse_upgrade_response_code(response) } + fn account_public_keys(&self, address: AccountAddress) -> QueryAccountPublicKeysResult { + query_account_public_keys_worker(address) + } + + fn check_account_signature( + &self, + address: AccountAddress, + signatures: &AccountSignatures, + data: &[u8], + ) -> CheckAccountSignatureResult { + check_account_signature_worker(address, signatures, data) + } + #[inline(always)] fn state(&self) -> &ExternStateApi { &self.state_api } diff --git a/concordium-std/src/test_infrastructure.rs b/concordium-std/src/test_infrastructure.rs index 99bec287..2b55fd81 100644 --- a/concordium-std/src/test_infrastructure.rs +++ b/concordium-std/src/test_infrastructure.rs @@ -1560,6 +1560,25 @@ impl + StateClone> fn state_and_builder(&mut self) -> (&mut State, &mut StateBuilder) { (&mut self.state, &mut self.state_builder) } + + fn account_public_keys(&self, _address: AccountAddress) -> QueryAccountPublicKeysResult { + unimplemented!( + "The test infrastructure will be deprecated and so does not implement new \ + functionality." + ) + } + + fn check_account_signature( + &self, + _address: AccountAddress, + _signatures: &AccountSignatures, + _data: &[u8], + ) -> CheckAccountSignatureResult { + unimplemented!( + "The test infrastructure will be deprecated and so does not implement new \ + functionality." + ) + } } impl> TestHost { diff --git a/concordium-std/src/traits.rs b/concordium-std/src/traits.rs index d04904cb..766ef12c 100644 --- a/concordium-std/src/traits.rs +++ b/concordium-std/src/traits.rs @@ -6,8 +6,9 @@ use crate::vec::Vec; use crate::{ types::{LogError, StateError}, - CallContractResult, EntryRaw, ExchangeRates, HashKeccak256, HashSha2256, HashSha3256, Key, - OccupiedEntryRaw, PublicKeyEcdsaSecp256k1, PublicKeyEd25519, QueryAccountBalanceResult, + AccountSignatures, CallContractResult, CheckAccountSignatureResult, EntryRaw, ExchangeRates, + HashKeccak256, HashSha2256, HashSha3256, Key, OccupiedEntryRaw, PublicKeyEcdsaSecp256k1, + PublicKeyEd25519, QueryAccountBalanceResult, QueryAccountPublicKeysResult, QueryContractBalanceResult, ReadOnlyCallContractResult, SignatureEcdsaSecp256k1, SignatureEd25519, StateBuilder, TransferResult, UpgradeResult, VacantEntryRaw, }; @@ -406,6 +407,25 @@ pub trait HasHost: Sized { /// including the amount transferred as part of the invocation. fn contract_balance(&self, address: ContractAddress) -> QueryContractBalanceResult; + /// Get the account's public keys. + fn account_public_keys(&self, address: AccountAddress) -> QueryAccountPublicKeysResult; + + /// Verify the signature with account's public keys. + /// + /// - `address` is the address of the account + /// - `signatures` is the [`AccountSignatures`] that are to be checked + /// - `data` is the data that the signatures are on. + /// + /// The response is an error if the account is missing, and if the + /// signatures were correctly parsed then it is a boolean indicating + /// whether the check succeeded or failed. + fn check_account_signature( + &self, + address: AccountAddress, + signatures: &AccountSignatures, + data: &[u8], + ) -> CheckAccountSignatureResult; + /// Get an immutable reference to the contract state. fn state(&self) -> &State; diff --git a/concordium-std/src/types.rs b/concordium-std/src/types.rs index 33043d1c..ab08964a 100644 --- a/concordium-std/src/types.rs +++ b/concordium-std/src/types.rs @@ -1,7 +1,10 @@ +use crate as concordium_std; use crate::{ cell::UnsafeCell, marker::PhantomData, num::NonZeroU32, Cursor, HasStateApi, Serial, Vec, }; -use concordium_contracts_common::{AccountBalance, Amount, ParseError}; +use concordium_contracts_common::{ + AccountBalance, AccountThreshold, Amount, ParseError, SchemaType, SignatureThreshold, +}; use core::{fmt, str::FromStr}; // Re-export for backward compatibility. pub use concordium_contracts_common::ExchangeRates; @@ -643,6 +646,25 @@ pub struct QueryAccountBalanceError; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct QueryContractBalanceError; +/// Error for querying account's public keys. +/// No account found for the provided account address. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct QueryAccountPublicKeysError; + +/// Error for checking an account signature. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CheckAccountSignatureError { + /// The account does not exist in the state. + MissingAccount, + /// The signature data could not be parsed, i.e., + /// we could not deserialize the signature map and the data to check the + /// signature against. This should typically not happen since the + /// `concordium-std` library prevents calls that could trigger it, but + /// is here for completeness since it is a possible error returned from + /// the node. + MalformedData, +} + /// A wrapper around [`Result`] that fixes the error variant to /// [`CallContractError`], and the result to `(bool, Option)`. /// If the result is `Ok` then the boolean indicates whether the state was @@ -672,6 +694,71 @@ pub type QueryAccountBalanceResult = Result; +/// A wrapper around [`Result`] that fixes the error variant to +/// [`QueryAccountPublicKeysError`] and result to [`AccountPublicKeys`]. +pub type QueryAccountPublicKeysResult = Result; + +/// A wrapper around [`Result`] that fixes the error variant to +/// [`CheckAccountSignatureError`] and result to [`bool`]. +pub type CheckAccountSignatureResult = Result; + +pub(crate) type KeyIndex = u8; + +#[derive(crate::Serialize, Debug, SchemaType)] +/// A public indexed by the signature scheme. Currently only a +/// single scheme is supported, `ed25519`. +pub(crate) enum PublicKey { + Ed25519(PublicKeyEd25519), +} + +#[derive(crate::Serialize, Debug, SchemaType)] +pub(crate) struct CredentialPublicKeys { + #[concordium(size_length = 1)] + pub(crate) keys: crate::collections::BTreeMap, + pub(crate) threshold: SignatureThreshold, +} + +#[derive(crate::Serialize, Debug, SchemaType)] +/// Public keys of an account, together with the thresholds. +/// This type is deliberately made opaque, but it has serialization instances +/// since inside smart contracts there is no need to inspect the values other +/// than to pass them to verification functions. +pub struct AccountPublicKeys { + #[concordium(size_length = 1)] + pub(crate) keys: crate::collections::BTreeMap, + pub(crate) threshold: AccountThreshold, +} + +pub(crate) type CredentialIndex = u8; + +#[derive(crate::Serialize, Debug, SchemaType)] +#[non_exhaustive] +/// A cryptographic signature indexed by the signature scheme. Currently only a +/// single scheme is supported, `ed25519`. +pub enum Signature { + Ed25519(SignatureEd25519), +} + +#[derive(crate::Serialize, Debug, SchemaType)] +#[concordium(transparent)] +/// Account signatures. This is an analogue of transaction signatures that are +/// part of transactions that get sent to the chain. +/// +/// This type is deliberately made opaque, but it has serialization instances. +/// It should be thought of as a nested map, indexed on the outer layer by +/// credential indexes, and the inner map maps key indices to [`Signature`]s. +pub struct AccountSignatures { + #[concordium(size_length = 1)] + pub(crate) sigs: crate::collections::BTreeMap, +} + +#[derive(crate::Serialize, Debug, SchemaType)] +#[concordium(transparent)] +pub(crate) struct CredentialSignatures { + #[concordium(size_length = 1)] + sigs: crate::collections::BTreeMap, +} + /// A type representing the attributes, lazily acquired from the host. #[derive(Clone, Copy, Default)] pub struct AttributesCursor { diff --git a/examples/README.md b/examples/README.md index 8c5a26c5..127c2fab 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,6 +8,8 @@ the logic of the contract is reasonable, or safe. **Do not use these contracts as-is for anything other then experimenting.** The list of contracts is as follows +- [account-signature-checks](./account-signature-checks) A simple contract that + demonstrates how account signature checks can be performed in smart contracts. - [two-step-transfer](./two-step-transfer) A contract that acts like an account (can send, store and accept CCD), but requires n > 1 ordained accounts to agree to the sending of CCD before it is accepted. - [auction](./auction) A contract implementing an simple auction. diff --git a/examples/account-signature-checks/Cargo.toml b/examples/account-signature-checks/Cargo.toml new file mode 100644 index 00000000..e5c025d7 --- /dev/null +++ b/examples/account-signature-checks/Cargo.toml @@ -0,0 +1,24 @@ +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package] +name = "account_signature_checks" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" +authors = [ "Concordium " ] +description = "An example of how to check account signatures" + +[features] +default = ["std"] +std = ["concordium-std/std"] +wee_alloc = ["concordium-std/wee_alloc"] + +[dependencies] +concordium-std = {path = "../../concordium-std", default-features = false} + +[lib] +crate-type=["cdylib", "rlib"] + +[profile.release] +opt-level = "s" +codegen-units = 1 diff --git a/examples/account-signature-checks/src/lib.rs b/examples/account-signature-checks/src/lib.rs new file mode 100644 index 00000000..b4b1175d --- /dev/null +++ b/examples/account-signature-checks/src/lib.rs @@ -0,0 +1,87 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +//! A basic example showing how to retrieve account keys, and check account +//! signatures. +use concordium_std::*; +use core::fmt::Debug; + +#[derive(Debug, PartialEq, Eq, Reject, Serial, SchemaType)] +enum Error { + /// Failed parsing the parameter. + #[from(ParseError)] + ParseParams, + /// Account that we wanted was not present. + MissingAccount, + /// Signature data was malformed. + MalformedData, +} + +impl From for Error { + fn from(value: CheckAccountSignatureError) -> Self { + match value { + CheckAccountSignatureError::MissingAccount => Self::MissingAccount, + CheckAccountSignatureError::MalformedData => Self::MalformedData, + } + } +} + +impl From for Error { + fn from(QueryAccountPublicKeysError: QueryAccountPublicKeysError) -> Self { + Self::MissingAccount + } +} + +/// We don't need state for this specific demonstration. +#[derive(Serialize)] +struct State {} + +/// Init function that creates a new smart contract. +#[init(contract = "account_signature_checks")] +fn init( + _ctx: &impl HasInitContext, + _state_builder: &mut StateBuilder, +) -> InitResult { + Ok(State {}) +} + +#[derive(Deserial, SchemaType)] +struct CheckParam { + address: AccountAddress, + sigs: AccountSignatures, + #[concordium(size_length = 4)] + data: Vec, +} + +/// View function that checks the signature with account keys on the provided +/// data. +#[receive( + contract = "account_signature_checks", + name = "check", + parameter = "CheckParam", + error = "Error", + return_value = "bool" +)] +fn check( + ctx: &impl HasReceiveContext, + host: &impl HasHost, +) -> Result { + let param: CheckParam = ctx.parameter_cursor().get()?; + let r = host.check_account_signature(param.address, ¶m.sigs, ¶m.data)?; + Ok(r) +} + +/// View function that returns the account's public keys. +#[receive( + contract = "account_signature_checks", + name = "view_keys", + parameter = "AccountAddress", + return_value = "AccountPublicKeys" +)] +fn view_keys( + ctx: &impl HasReceiveContext, + host: &impl HasHost, +) -> Result { + let param: AccountAddress = ctx.parameter_cursor().get()?; + let pk = host.account_public_keys(param)?; + Ok(pk) +}