From 87bbcca70f9917023b1c57aec011db47ffd27f57 Mon Sep 17 00:00:00 2001 From: Santiago Pittella <87827390+SantiagoPittella@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:13:29 -0300 Subject: [PATCH 1/4] feat: add tracing for proxy and worker (#1014) review: rename to MIDEN_TX_PROVER, add target to parent trace for each request review: use default implementations directly from trait review: unpin patch versions in cargo.toml review: remove debug log with transaction witness chore: remove unused import review: improve comment on default methods implementation review: remove info! log and change log level of the prove_transaction method return review: use log level from env review: improve tracing setup documentation review: refactor logging and tracing sections in readme review: add transaction ID to worker trace review: merge tracing setup functions fix: add default implementation proxy review: tracing section in readme shorter review: address readme comments fix: add missing import review: move changelog entry to bottom review: add missing empty line in readme review: exclude parameters from trace in connected_to_upstream --- CHANGELOG.md | 3 +- Cargo.lock | 223 +++++++++++++++++++++++++++ bin/tx-prover/Cargo.toml | 9 +- bin/tx-prover/README.md | 22 ++- bin/tx-prover/src/api/mod.rs | 18 ++- bin/tx-prover/src/commands/mod.rs | 6 +- bin/tx-prover/src/commands/proxy.rs | 6 +- bin/tx-prover/src/commands/worker.rs | 5 +- bin/tx-prover/src/main.rs | 2 +- bin/tx-prover/src/proxy/mod.rs | 166 +++++++++++++++++++- bin/tx-prover/src/utils.rs | 93 ++++++++++- 11 files changed, 530 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53df31655..f9c94b0fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Changes -- Removed workers list from the proxy configuration file (#1018). +- Added tracing to the `miden-tx-prover` CLI (#1014). - Added health check endpoints to the prover service (#1006). - Implemented serialization for `AccountHeader` (#996). - Updated Pingora crates to 0.4 and added polling time to the configuration file (#997). @@ -18,6 +18,7 @@ - Added conversion from `Account` to `AccountDelta` for initial account state representation as delta (#983). - [BREAKING] Added `miden::note::get_script_hash` procedure (#995). - [BREAKING] Refactor error messages in `miden-lib` and `miden-tx` and use `thiserror` 2.0 (#1005). +- Removed workers list from the proxy configuration file (#1018). ## 0.6.2 (2024-11-20) diff --git a/Cargo.lock b/Cargo.lock index 5b3ec503f..ae5a66c47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1122,6 +1122,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -1474,6 +1480,12 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "integer-encoding" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" + [[package]] name = "ipnet" version = "2.10.1" @@ -2002,6 +2014,7 @@ version = "0.7.0" dependencies = [ "async-trait", "axum", + "bytes", "clap 4.5.21", "figment", "getrandom", @@ -2010,6 +2023,11 @@ dependencies = [ "miden-tx", "miette", "once_cell", + "opentelemetry 0.27.1", + "opentelemetry-jaeger", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions 0.27.0", + "opentelemetry_sdk 0.27.1", "pingora", "pingora-core", "pingora-limits", @@ -2029,6 +2047,7 @@ dependencies = [ "tonic-web", "tonic-web-wasm-client", "tracing", + "tracing-opentelemetry", "tracing-subscriber", "uuid", "winter-maybe-async", @@ -2239,6 +2258,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + [[package]] name = "object" version = "0.36.5" @@ -2304,6 +2333,150 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror 1.0.69", +] + +[[package]] +name = "opentelemetry" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab70038c28ed37b97d8ed414b6429d343a8bbf44c9f79ec854f3a643029ba6d7" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "opentelemetry-jaeger" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501b471b67b746d9a07d4c29f8be00f952d1a2eca356922ede0098cbaddff19f" +dependencies = [ + "async-trait", + "futures-core", + "futures-util", + "opentelemetry 0.23.0", + "opentelemetry-semantic-conventions 0.15.0", + "opentelemetry_sdk 0.23.0", + "thrift", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" +dependencies = [ + "async-trait", + "futures-core", + "http 1.1.0", + "opentelemetry 0.27.1", + "opentelemetry-proto", + "opentelemetry_sdk 0.27.1", + "prost", + "thiserror 1.0.69", + "tokio", + "tonic", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" +dependencies = [ + "opentelemetry 0.27.1", + "opentelemetry_sdk 0.27.1", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1869fb4bb9b35c5ba8a1e40c9b128a7b4c010d07091e864a29da19e4fe2ca4d7" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc1b6902ff63b32ef6c489e8048c5e253e2e4a803ea3ea7e783914536eb15c52" + +[[package]] +name = "opentelemetry_sdk" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae312d58eaa90a82d2e627fd86e075cf5230b3f11794e2ed74199ebbe572d4fd" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "lazy_static", + "once_cell", + "opentelemetry 0.23.0", + "ordered-float 4.5.0", + "percent-encoding", + "rand", + "thiserror 1.0.69", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry 0.27.1", + "percent-encoding", + "rand", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-float" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c65ee1f9701bf938026630b455d5315f490640234259037edb259798b3bcf85e" +dependencies = [ + "num-traits", +] + [[package]] name = "os_str_bytes" version = "6.6.1" @@ -3716,6 +3889,28 @@ dependencies = [ "once_cell", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "thrift" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e54bc85fc7faa8bc175c4bab5b92ba8d9a3ce893d0e9f42cc455c8ab16a9e09" +dependencies = [ + "byteorder", + "integer-encoding", + "log", + "ordered-float 2.10.1", + "threadpool", +] + [[package]] name = "thrift_codec" version = "0.1.1" @@ -4075,6 +4270,24 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a971f6058498b5c0f1affa23e7ea202057a7301dbff68e968b2d578bcbd053" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry 0.27.1", + "opentelemetry_sdk 0.27.1", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-serde" version = "0.2.0" @@ -4400,6 +4613,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/bin/tx-prover/Cargo.toml b/bin/tx-prover/Cargo.toml index 36b2b6182..174b3c8eb 100644 --- a/bin/tx-prover/Cargo.toml +++ b/bin/tx-prover/Cargo.toml @@ -32,12 +32,19 @@ tonic = { version = "0.12", default-features = false, features = ["prost", "code getrandom = { version = "0.2", features = ["js"], optional = true } [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] +bytes = "1.0" tonic = { version = "0.12", default-features = false, features = ["prost", "codegen", "transport"] } -once_cell = "1.19.0" +once_cell = "1.19" pingora = { version = "0.4", features = [ "lb" ] } pingora-core = "0.4" pingora-proxy = "0.4" pingora-limits = "0.4" +opentelemetry = { version = "0.27", features = ["metrics", "trace"] } +opentelemetry-otlp = { version = "0.27", features = ["grpc-tonic"] } +opentelemetry_sdk = { version = "0.27", features = ["metrics", "rt-tokio"] } +opentelemetry-semantic-conventions = "0.27" +opentelemetry-jaeger = "0.22" +tracing-opentelemetry = "0.28" [dependencies] async-trait = "0.1" diff --git a/bin/tx-prover/README.md b/bin/tx-prover/README.md index 080d19d89..7dba23387 100644 --- a/bin/tx-prover/README.md +++ b/bin/tx-prover/README.md @@ -98,9 +98,27 @@ The worker service implements the [gRPC Health Check](https://grpc.io/docs/guide The proxy service uses this health check to determine if a worker is available to receive requests. If a worker is not available, it will be removed from the set of workers that the proxy can use to send requests. -## Logging +## Logging and Tracing -Both the worker and the proxy will use the `info` log level by default, but it can be changed by setting the `RUST_LOG` environment variable. +The service uses the [`tracing`](https://docs.rs/tracing/latest/tracing/) crate for both logging and distributed tracing, providing structured, high-performance logs and trace data. + +By default, logs are written to `stdout` and the default logging level is `info`. This can be changed via the `RUST_LOG` environment variable. For example: + +``` +export RUST_LOG=debug +``` + +For tracing, we use OpenTelemetry protocol. By default, traces are exported to the endpoint specified by `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. To consume and visualize these traces we can use Jaeger or any other OpenTelemetry compatible consumer. + +The simplest way to install Jaeger is by using a [Docker](https://www.docker.com/) container. To do so, run: + +```bash +docker run -d -p4317:4317 -p16686:16686 jaegertracing/all-in-one:latest +``` + +Then access the Jaeger UI at `http://localhost:16686/`. + +If Docker is not an option, Jaeger can also be set up directly on your machine or hosted in the cloud. See the [Jaeger documentation](https://www.jaegertracing.io/docs/) for alternative installation methods. ## Features diff --git a/bin/tx-prover/src/api/mod.rs b/bin/tx-prover/src/api/mod.rs index c71fb344d..0a9a9e235 100644 --- a/bin/tx-prover/src/api/mod.rs +++ b/bin/tx-prover/src/api/mod.rs @@ -9,7 +9,9 @@ use miden_tx_prover::generated::{ }; use tokio::{net::TcpListener, sync::Mutex}; use tonic::{Request, Response, Status}; -use tracing::info; +use tracing::instrument; + +use crate::utils::MIDEN_TX_PROVER; pub struct RpcListener { pub api_service: ApiServer, @@ -30,12 +32,18 @@ pub struct ProverRpcApi { #[async_trait::async_trait] impl ProverApi for ProverRpcApi { + #[instrument( + target = MIDEN_TX_PROVER, + name = "prover:prove_transaction", + skip_all, + ret(level = "debug"), + fields(transaction_id = tracing::field::Empty), + err + )] async fn prove_transaction( &self, request: Request, ) -> Result, tonic::Status> { - info!("Received request to prove transaction"); - // Try to acquire a permit without waiting let prover = self .local_prover @@ -48,6 +56,10 @@ impl ProverApi for ProverRpcApi { let proof = prover.prove(transaction_witness).map_err(internal_error)?; + // Record the transaction_id in the current tracing span + let transaction_id = proof.id(); + tracing::Span::current().record("transaction_id", tracing::field::display(&transaction_id)); + Ok(Response::new(ProveTransactionResponse { proven_transaction: proof.to_bytes() })) } } diff --git a/bin/tx-prover/src/commands/mod.rs b/bin/tx-prover/src/commands/mod.rs index 81cd6015e..8d9f494f5 100644 --- a/bin/tx-prover/src/commands/mod.rs +++ b/bin/tx-prover/src/commands/mod.rs @@ -7,9 +7,12 @@ use init::Init; use miden_tx_prover::PROVER_SERVICE_CONFIG_FILE_NAME; use proxy::StartProxy; use serde::{Deserialize, Serialize}; +use tracing::instrument; use update_workers::{AddWorkers, RemoveWorkers, UpdateWorkers}; use worker::StartWorker; +use crate::utils::MIDEN_TX_PROVER; + pub mod init; pub mod proxy; pub mod update_workers; @@ -19,7 +22,7 @@ pub mod worker; /// /// It is stored in a TOML file, which will be created by the `init` command. /// It allows manual modification of the configuration file. -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ProxyConfig { /// Host of the proxy. pub host: String, @@ -111,6 +114,7 @@ pub enum Command { /// CLI entry point impl Cli { + #[instrument(target = MIDEN_TX_PROVER, name = "cli:execute", skip_all, ret(level = "info"), err)] pub async fn execute(&self) -> Result<(), String> { match &self.action { // For the `StartWorker` command, we need to create a new runtime and run the worker diff --git a/bin/tx-prover/src/commands/proxy.rs b/bin/tx-prover/src/commands/proxy.rs index 1eb693cfa..b0a9edd31 100644 --- a/bin/tx-prover/src/commands/proxy.rs +++ b/bin/tx-prover/src/commands/proxy.rs @@ -8,7 +8,10 @@ use pingora::{ use pingora_proxy::http_proxy_service; use tracing::warn; -use crate::proxy::{LoadBalancer, LoadBalancerState}; +use crate::{ + proxy::{LoadBalancer, LoadBalancerState}, + utils::MIDEN_TX_PROVER, +}; /// Starts the proxy defined in the config file. /// @@ -27,6 +30,7 @@ impl StartProxy { /// /// This method will first read the config file to get the parameters for the proxy. It will /// then start a proxy with each worker passed as command argument as a backend. + #[tracing::instrument(target = MIDEN_TX_PROVER, name = "proxy:execute")] pub async fn execute(&self) -> Result<(), String> { let mut server = Server::new(Some(Opt::default())).map_err(|err| err.to_string())?; server.bootstrap(); diff --git a/bin/tx-prover/src/commands/worker.rs b/bin/tx-prover/src/commands/worker.rs index eeb965d8b..7aa65f20d 100644 --- a/bin/tx-prover/src/commands/worker.rs +++ b/bin/tx-prover/src/commands/worker.rs @@ -3,9 +3,9 @@ use miden_tx_prover::generated::api_server::ApiServer; use tokio::net::TcpListener; use tokio_stream::wrappers::TcpListenerStream; use tonic_health::server::health_reporter; -use tracing::info; +use tracing::{info, instrument}; -use crate::api::RpcListener; +use crate::{api::RpcListener, utils::MIDEN_TX_PROVER}; /// Starts a worker. #[derive(Debug, Parser)] @@ -28,6 +28,7 @@ impl StartWorker { /// The worker includes a health reporter that will mark the service as serving, following the /// [gRPC health checking protocol]( /// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto). + #[instrument(target = MIDEN_TX_PROVER, name = "worker:execute")] pub async fn execute(&self) -> Result<(), String> { let worker_addr = format!("{}:{}", self.host, self.port); let rpc = diff --git a/bin/tx-prover/src/main.rs b/bin/tx-prover/src/main.rs index 278ae2570..176a353cf 100644 --- a/bin/tx-prover/src/main.rs +++ b/bin/tx-prover/src/main.rs @@ -9,7 +9,7 @@ use utils::setup_tracing; async fn main() -> Result<(), String> { use clap::Parser; - setup_tracing(); + setup_tracing()?; // read command-line args let cli = Cli::parse(); diff --git a/bin/tx-prover/src/proxy/mod.rs b/bin/tx-prover/src/proxy/mod.rs index 0ceacb1ca..49ec0d079 100644 --- a/bin/tx-prover/src/proxy/mod.rs +++ b/bin/tx-prover/src/proxy/mod.rs @@ -1,10 +1,13 @@ use std::{collections::VecDeque, future::Future, pin::Pin, sync::Arc, time::Duration}; use async_trait::async_trait; +use bytes::Bytes; use once_cell::sync::Lazy; use pingora::{ + http::ResponseHeader, lb::Backend, prelude::*, + protocols::Digest, server::ShutdownWatch, services::background::BackgroundService, upstreams::peer::{Peer, ALPN}, @@ -13,7 +16,7 @@ use pingora_core::{upstreams::peer::HttpPeer, Result}; use pingora_limits::rate::Rate; use pingora_proxy::{ProxyHttp, Session}; use tokio::{sync::RwLock, time::sleep}; -use tracing::{error, info, warn}; +use tracing::{debug_span, error, info, info_span, warn, Span}; use uuid::Uuid; use worker::Worker; @@ -24,7 +27,7 @@ use crate::{ }, utils::{ create_queue_full_response, create_response_with_error_message, - create_too_many_requests_response, create_workers_updated_response, + create_too_many_requests_response, create_workers_updated_response, MIDEN_TX_PROVER, }, }; @@ -37,6 +40,7 @@ const LOCALHOST_ADDR: &str = "127.0.0.1"; // ================================================================================================ /// Load balancer that uses a round robin strategy +#[derive(Debug)] pub struct LoadBalancerState { workers: Arc>>, timeout_secs: Duration, @@ -50,6 +54,7 @@ pub struct LoadBalancerState { impl LoadBalancerState { /// Create a new load balancer + #[tracing::instrument(name = "proxy:new_load_balancer", skip(initial_workers))] pub async fn new( initial_workers: Vec, config: &ProxyConfig, @@ -291,6 +296,7 @@ static QUEUE: Lazy = Lazy::new(RequestQueue::new); /// Custom context for the request/response lifecycle /// We use this context to keep track of the number of tries for a request, the unique ID for the /// request, and the worker that will process the request. +#[derive(Debug)] pub struct RequestContext { /// Number of tries for the request tries: usize, @@ -298,15 +304,19 @@ pub struct RequestContext { request_id: Uuid, /// Worker that will process the request worker: Option, + /// Parent span for the request + parent_span: Span, } impl RequestContext { /// Create a new request context fn new() -> Self { + let request_id = Uuid::new_v4(); Self { tries: 0, - request_id: Uuid::new_v4(), + request_id, worker: None, + parent_span: info_span!(target: MIDEN_TX_PROVER, "proxy:new_request", request_id = request_id.to_string()), } } @@ -324,6 +334,7 @@ impl RequestContext { /// This wrapper is used to implement the ProxyHttp trait for Arc. /// This is necessary because we want to share the load balancer between the proxy server and the /// health check background service. +#[derive(Debug)] pub struct LoadBalancer(pub Arc); /// Implements load-balancing of incoming requests across a pool of workers. @@ -358,6 +369,7 @@ impl ProxyHttp for LoadBalancer { /// Here we apply IP-based rate-limiting to the request. We also check if the queue is full. /// /// If the request is rate-limited, we return a 429 response. Otherwise, we return false. + #[tracing::instrument(name = "proxy:request_filter", parent = &ctx.parent_span, skip(session))] async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result where Self::CTX: Send + Sync, @@ -414,6 +426,7 @@ impl ProxyHttp for LoadBalancer { /// /// Note that the request will be assigned a worker here, and the worker will be removed from /// the list of available workers once it reaches the [Self::logging] method. + #[tracing::instrument(name = "proxy:upstream_peer", parent = &ctx.parent_span, skip(_session))] async fn upstream_peer( &self, _session: &mut Session, @@ -473,6 +486,7 @@ impl ProxyHttp for LoadBalancer { /// /// This method is called right after [Self::upstream_peer()] returns a [HttpPeer] and a /// connection is established with the worker. + #[tracing::instrument(name = "proxy:upstream_request_filter", parent = &_ctx.parent_span, skip(_session))] async fn upstream_request_filter( &self, _session: &mut Session, @@ -494,6 +508,7 @@ impl ProxyHttp for LoadBalancer { } /// Retry the request if the connection fails. + #[tracing::instrument(name = "proxy:fail_to_connect", parent = &ctx.parent_span, skip(_session))] fn fail_to_connect( &self, _session: &mut Session, @@ -513,6 +528,7 @@ impl ProxyHttp for LoadBalancer { /// /// This method is the last one in the request lifecycle, no matter if the request was /// processed or not. + #[tracing::instrument(name = "proxy:logging", parent = &ctx.parent_span, skip(_session))] async fn logging(&self, _session: &mut Session, e: Option<&Error>, ctx: &mut Self::CTX) where Self::CTX: Send + Sync, @@ -526,6 +542,146 @@ impl ProxyHttp for LoadBalancer { self.0.add_available_worker(worker).await; } } + + // The following methods are a copy of the default implementation defined in the trait, but + // with tracing instrumentation. + // Pingora calls these methods to handle the request/response lifecycle internally and since + // the trait is defined in a different crate, we cannot add the tracing instrumentation there. + // We use the default implementation by implementing the method for our specific type, adding + // the tracing instrumentation and internally calling `ProxyHttp` methods. + // ============================================================================================ + #[tracing::instrument(name = "proxy:early_request_filter", parent = &ctx.parent_span, skip(_session))] + async fn early_request_filter( + &self, + _session: &mut Session, + ctx: &mut Self::CTX, + ) -> Result<()> { + ProxyHttpDefaultImpl.early_request_filter(_session, &mut ()).await + } + + #[tracing::instrument(name = "proxy:connected_to_upstream", parent = &ctx.parent_span, skip(_session, _sock, _reused, _peer, _fd, _digest))] + async fn connected_to_upstream( + &self, + _session: &mut Session, + _reused: bool, + _peer: &HttpPeer, + #[cfg(unix)] _fd: std::os::unix::io::RawFd, + #[cfg(windows)] _sock: std::os::windows::io::RawSocket, + _digest: Option<&Digest>, + ctx: &mut Self::CTX, + ) -> Result<()> { + ProxyHttpDefaultImpl + .connected_to_upstream(_session, _reused, _peer, _fd, _digest, &mut ()) + .await + } + + #[tracing::instrument(name = "proxy:request_body_filter", parent = &ctx.parent_span, skip(_session, _body))] + async fn request_body_filter( + &self, + _session: &mut Session, + _body: &mut Option, + _end_of_stream: bool, + ctx: &mut Self::CTX, + ) -> Result<()> { + ProxyHttpDefaultImpl + .request_body_filter(_session, _body, _end_of_stream, &mut ()) + .await + } + + #[tracing::instrument(name = "proxy:upstream_response_filter", parent = &ctx.parent_span, skip(_session, _upstream_response))] + fn upstream_response_filter( + &self, + _session: &mut Session, + _upstream_response: &mut ResponseHeader, + ctx: &mut Self::CTX, + ) { + ProxyHttpDefaultImpl.upstream_response_filter(_session, _upstream_response, &mut ()) + } + + #[tracing::instrument(name = "proxy:response_filter", parent = &ctx.parent_span, skip(_session, _upstream_response))] + async fn response_filter( + &self, + _session: &mut Session, + _upstream_response: &mut ResponseHeader, + ctx: &mut Self::CTX, + ) -> Result<()> + where + Self::CTX: Send + Sync, + { + ProxyHttpDefaultImpl + .response_filter(_session, _upstream_response, &mut ()) + .await + } + + #[tracing::instrument(name = "proxy:upstream_response_body_filter", parent = &ctx.parent_span, skip(_session, _body))] + fn upstream_response_body_filter( + &self, + _session: &mut Session, + _body: &mut Option, + _end_of_stream: bool, + ctx: &mut Self::CTX, + ) { + ProxyHttpDefaultImpl.upstream_response_body_filter(_session, _body, _end_of_stream, &mut ()) + } + + #[tracing::instrument(name = "proxy:response_body_filter", parent = &ctx.parent_span, skip(_session, _body))] + fn response_body_filter( + &self, + _session: &mut Session, + _body: &mut Option, + _end_of_stream: bool, + ctx: &mut Self::CTX, + ) -> Result> + where + Self::CTX: Send + Sync, + { + ProxyHttpDefaultImpl.response_body_filter(_session, _body, _end_of_stream, &mut ()) + } + + #[tracing::instrument(name = "proxy:fail_to_proxy", parent = &ctx.parent_span, skip(session))] + async fn fail_to_proxy(&self, session: &mut Session, e: &Error, ctx: &mut Self::CTX) -> u16 + where + Self::CTX: Send + Sync, + { + ProxyHttpDefaultImpl.fail_to_proxy(session, e, &mut ()).await + } + + #[tracing::instrument(name = "proxy:error_while_proxy", parent = &ctx.parent_span, skip(session))] + fn error_while_proxy( + &self, + peer: &HttpPeer, + session: &mut Session, + e: Box, + ctx: &mut Self::CTX, + client_reused: bool, + ) -> Box { + ProxyHttpDefaultImpl.error_while_proxy(peer, session, e, &mut (), client_reused) + } +} + +// PROXY HTTP DEFAULT IMPLEMENTATION +// ================================================================================================ + +/// Default implementation of the [ProxyHttp] trait. +/// +/// It is used to provide the default methods of the trait in order for the [LoadBalancer] to +/// implement the trait adding tracing instrumentation but without having to copy all default +/// implementations. +struct ProxyHttpDefaultImpl; + +#[async_trait] +impl ProxyHttp for ProxyHttpDefaultImpl { + type CTX = (); + fn new_ctx(&self) {} + + /// This method is the only one that does not have a default implementation in the trait. + async fn upstream_peer( + &self, + _session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + unimplemented!("This is a dummy implementation, should not be called") + } } /// Implement the BackgroundService trait for the LoadBalancer @@ -557,6 +713,10 @@ impl BackgroundService for LoadBalancerState { { Box::pin(async move { loop { + // Create a new spawn to perform the health check + let span = debug_span!("proxy:health_check"); + let _guard = span.enter(); + let mut workers = self.workers.write().await; // Perform health checks on workers and retain healthy ones diff --git a/bin/tx-prover/src/utils.rs b/bin/tx-prover/src/utils.rs index 6e310d6d6..5e6c3e2bc 100644 --- a/bin/tx-prover/src/utils.rs +++ b/bin/tx-prover/src/utils.rs @@ -1,18 +1,95 @@ use std::time::Duration; +use opentelemetry::{trace::TracerProvider as _, KeyValue}; +use opentelemetry_sdk::{ + runtime, + trace::{RandomIdGenerator, Sampler, TracerProvider}, + Resource, +}; +use opentelemetry_semantic_conventions::{ + resource::{SERVICE_NAME, SERVICE_VERSION}, + SCHEMA_URL, +}; use pingora::{http::ResponseHeader, Error, ErrorType}; use pingora_proxy::Session; use tonic::transport::Channel; use tonic_health::pb::health_client::HealthClient; +use tracing_subscriber::{layer::SubscriberExt, Registry}; + +pub const MIDEN_TX_PROVER: &str = "miden-tx-prover"; const RESOURCE_EXHAUSTED_CODE: u16 = 8; -pub(crate) fn setup_tracing() { - // Set a default log level if `RUST_LOG` is not set - if std::env::var("RUST_LOG").is_err() { - std::env::set_var("RUST_LOG", "info"); // Default to 'info' level - } - tracing_subscriber::fmt::init(); +/// Initializes and configures the global tracing and telemetry system for the CLI, worker and +/// proxy services. +/// +/// This function sets up a tracing pipeline that includes: +/// +/// - An OpenTelemetry (OTLP) exporter, which sends span data to an OTLP endpoint using gRPC. +/// - A [TracerProvider] configured with a [Sampler::ParentBased] sampler at a `1.0` sampling ratio, +/// ensuring that all traces are recorded. +/// - A resource containing the service name and version extracted from the crate's metadata. +/// - A `tracing` subscriber that integrates the configured [TracerProvider] with the Rust `tracing` +/// ecosystem, applying filters from the environment and enabling formatted console logs. +/// +/// **Process:** +/// 1. **OTLP Exporter**: Creates an OTLP span exporter that sends trace data to a collector +/// endpoint. If it fails to create the exporter, returns an error describing the failure. +/// +/// 2. **Resource Setup**: Creates a [Resource] containing service metadata (name and version), +/// which is attached to all emitted telemetry data to identify the originating service. +/// +/// 3. **TracerProvider and Sampler**: Builds a [TracerProvider] using a [Sampler::ParentBased] +/// sampler layered over a [Sampler::TraceIdRatioBased] sampler set to `1.0`, ensuring all traces +/// are recorded. A random ID generator is used to produce trace and span IDs. The tracer is +/// retrieved from this provider, which can then be used by the OpenTelemetry layer of `tracing`. +/// +/// 4. **Telemetry Integration with tracing**: Creates a telemetry layer from +/// `tracing_opentelemetry` and combines it with a `Registry` subscriber and a formatting layer. +/// This results in a subscriber stack that: +/// - Sends telemetry to the OTLP exporter. +/// - Filters logs/spans based on environment variables. +/// - Pretty-prints formatted logs to stdout. +/// +/// 5. **Global Subscriber**: Finally, sets this composite subscriber as the global default. If +/// this fails (e.g., if a global subscriber is already set), an error will be returned. +/// +/// **Returns:** +/// - `Ok(())` if the global subscriber is successfully set up. +/// - `Err(String)` describing the failure if any step (creating the exporter or setting the +/// subscriber) fails. +pub(crate) fn setup_tracing() -> Result<(), String> { + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .build() + .map_err(|e| format!("Failed to create OTLP exporter: {:?}", e))?; + + let resource = Resource::from_schema_url( + [ + KeyValue::new(SERVICE_NAME, env!("CARGO_PKG_NAME")), + KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION")), + ], + SCHEMA_URL, + ); + + let provider = TracerProvider::builder() + .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(1.0)))) + .with_id_generator(RandomIdGenerator::default()) + .with_resource(resource) + .with_batch_exporter(exporter, runtime::Tokio) + .build(); + + let tracer = provider.tracer(MIDEN_TX_PROVER); + + let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); + + let subscriber = Registry::default() + .with(telemetry) + .with(tracing_subscriber::filter::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer()); + + tracing::subscriber::set_global_default(subscriber) + .map_err(|e| format!("Failed to set subscriber: {:?}", e)) } /// Create a 503 response for a full queue @@ -88,11 +165,11 @@ pub async fn create_health_check_client( total_timeout: Duration, ) -> Result, String> { Channel::from_shared(format!("http://{}", address)) - .map_err(|err| err.to_string())? + .map_err(|err| format!("Invalid format for worker URI: {}", err))? .connect_timeout(connection_timeout) .timeout(total_timeout) .connect() .await .map(HealthClient::new) - .map_err(|err| err.to_string()) + .map_err(|err| format!("Failed to create health check client for worker: {}", err)) } From f8903dc1b03f70d82616265298dcad567b286f16 Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Thu, 19 Dec 2024 22:40:34 -0800 Subject: [PATCH 2/4] chore: fix typos in docs/comments --- miden-lib/asm/kernels/transaction/api.masm | 2 +- .../asm/kernels/transaction/lib/memory.masm | 26 +++++++++---------- miden-lib/asm/kernels/transaction/lib/tx.masm | 2 +- miden-lib/asm/miden/tx.masm | 2 +- miden-lib/masm_doc_comment_fmt.md | 2 +- miden-lib/src/transaction/memory.rs | 4 +-- objects/src/assets/mod.rs | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/miden-lib/asm/kernels/transaction/api.masm b/miden-lib/asm/kernels/transaction/api.masm index 79f4dfdef..eb4b149b4 100644 --- a/miden-lib/asm/kernels/transaction/api.masm +++ b/miden-lib/asm/kernels/transaction/api.masm @@ -664,7 +664,7 @@ end #! - note_type is the note storage type. #! - execution_hint is the note execution hint tag and payload. #! - RECIPIENT is the recipient of the note. -#! - note_idx is the index of the crated note. +#! - note_idx is the index of the created note. #! #! Panics if: #! - the procedure is called from a non-account context. diff --git a/miden-lib/asm/kernels/transaction/lib/memory.masm b/miden-lib/asm/kernels/transaction/lib/memory.masm index 0ed627ea7..8083b4da0 100644 --- a/miden-lib/asm/kernels/transaction/lib/memory.masm +++ b/miden-lib/asm/kernels/transaction/lib/memory.masm @@ -156,7 +156,7 @@ const.INPUT_NOTE_DATA_SECTION_OFFSET=1064960 # The memory address at which the number of input notes is stored. const.NUM_INPUT_NOTES_PTR=1048576 -# The offsets at which data of a input note is stored relative to the start of its data segment +# The offsets at which data of an input note is stored relative to the start of its data segment const.INPUT_NOTE_ID_OFFSET=0 const.INPUT_NOTE_CORE_DATA_OFFSET=1 const.INPUT_NOTE_SERIAL_NUM_OFFSET=1 @@ -174,7 +174,7 @@ const.INPUT_NOTE_ASSETS_OFFSET=8 # The memory address at which the output notes section begins. const.OUTPUT_NOTE_SECTION_OFFSET=4194304 -# The offsets at which data of a output note is stored relative to the start of its data segment. +# The offsets at which data of an output note is stored relative to the start of its data segment. const.OUTPUT_NOTE_ID_OFFSET=0 const.OUTPUT_NOTE_METADATA_OFFSET=1 const.OUTPUT_NOTE_RECIPIENT_OFFSET=2 @@ -1023,7 +1023,7 @@ export.set_num_input_notes push.NUM_INPUT_NOTES_PTR mem_store end -#! Computes a pointer to the memory address at which the data associated with a input note with +#! Computes a pointer to the memory address at which the data associated with an input note with #! index `idx` is stored. #! #! Inputs: [idx] @@ -1061,7 +1061,7 @@ export.get_input_note_nullifier_ptr push.INPUT_NOTE_SECTION_OFFSET.1 add add end -#! Returns the nullifier of a input note with `idx`. +#! Returns the nullifier of an input note with `idx`. #! #! Inputs: [idx] #! Outputs: [nullifier] @@ -1086,7 +1086,7 @@ export.get_input_note_core_ptr push.INPUT_NOTE_CORE_DATA_OFFSET add end -#! Returns the script root of a input note located at the specified memory address. +#! Returns the script root of an input note located at the specified memory address. #! #! Inputs: [note_ptr] #! Outputs: [SCRIPT_HASH] @@ -1100,7 +1100,7 @@ export.get_input_note_script_root mem_loadw end -#! Returns the memory address of the script root of a input note. +#! Returns the memory address of the script root of an input note. #! #! Inputs: [note_ptr] #! Outputs: [script_root_ptr] @@ -1112,7 +1112,7 @@ export.get_input_note_script_root_ptr push.INPUT_NOTE_SCRIPT_ROOT_OFFSET add end -#! Returns the inputs hash of a input note located at the specified memory address. +#! Returns the inputs hash of an input note located at the specified memory address. #! #! Inputs: [note_ptr] #! Outputs: [INPUTS_HASH] @@ -1126,7 +1126,7 @@ export.get_input_note_inputs_hash mem_loadw end -#! Returns the metadata of a input note located at the specified memory address. +#! Returns the metadata of an input note located at the specified memory address. #! #! Inputs: [note_ptr] #! Outputs: [METADATA] @@ -1140,7 +1140,7 @@ export.get_input_note_metadata mem_loadw end -#! Sets the metadata for a input note located at the specified memory address. +#! Sets the metadata for an input note located at the specified memory address. #! #! Inputs: [note_ptr, NOTE_METADATA] #! Outputs: [NOTE_METADATA] @@ -1167,7 +1167,7 @@ export.get_input_note_args mem_loadw end -#! Sets the note args for a input note located at the specified memory address. +#! Sets the note args for an input note located at the specified memory address. #! #! Inputs: [note_ptr, NOTE_ARGS] #! Outputs: [NOTE_ARGS] @@ -1193,7 +1193,7 @@ export.get_input_note_num_assets mem_load end -#! Sets the number of assets for a input note located at the specified memory address. +#! Sets the number of assets for an input note located at the specified memory address. #! #! Inputs: [note_ptr, num_assets] #! Outputs: [] @@ -1226,7 +1226,7 @@ end #! #! Where: #! - note_ptr is the memory address at which the input note data begins. -#! - ASSET_HASH is the sequential hash of the padded assets of a input note. +#! - ASSET_HASH is the sequential hash of the padded assets of an input note. export.get_input_note_assets_hash padw movup.4 push.INPUT_NOTE_ASSETS_HASH_OFFSET add @@ -1279,7 +1279,7 @@ export.get_output_note_data_offset push.OUTPUT_NOTE_SECTION_OFFSET end -#! Computes a pointer to the memory address at which the data associated with a output note with +#! Computes a pointer to the memory address at which the data associated with an output note with #! index `i` is stored. #! #! Inputs: [i] diff --git a/miden-lib/asm/kernels/transaction/lib/tx.masm b/miden-lib/asm/kernels/transaction/lib/tx.masm index a71d9ae4f..87245bf51 100644 --- a/miden-lib/asm/kernels/transaction/lib/tx.masm +++ b/miden-lib/asm/kernels/transaction/lib/tx.masm @@ -407,7 +407,7 @@ end #! or off-chain). #! - execution_hint is the hint which specifies when a note is ready to be consumed. #! - RECIPIENT defines spend conditions for the note. -#! - note_idx is the index of the crated note. +#! - note_idx is the index of the created note. #! #! Panics if: #! - the note_type is not valid. diff --git a/miden-lib/asm/miden/tx.masm b/miden-lib/asm/miden/tx.masm index dfc6dcc99..85d2a6500 100644 --- a/miden-lib/asm/miden/tx.masm +++ b/miden-lib/asm/miden/tx.masm @@ -114,7 +114,7 @@ end #! - note_type is the storage type of the note. #! - execution_hint is the note's execution hint. #! - RECIPIENT is the recipient of the note. -#! - note_idx is the index of the crated note. +#! - note_idx is the index of the created note. #! #! Invocation: exec export.create_note diff --git a/miden-lib/masm_doc_comment_fmt.md b/miden-lib/masm_doc_comment_fmt.md index bc8cac122..e450d8c41 100644 --- a/miden-lib/masm_doc_comment_fmt.md +++ b/miden-lib/masm_doc_comment_fmt.md @@ -218,7 +218,7 @@ Example: #! - note_type is the storage type of the note. #! - execution_hint is the note's execution hint. #! - RECIPIENT is the recipient of the note. -#! - note_idx is the index of the crated note. +#! - note_idx is the index of the created note. ``` ## Panic block diff --git a/miden-lib/src/transaction/memory.rs b/miden-lib/src/transaction/memory.rs index 58c56831b..7d810c26d 100644 --- a/miden-lib/src/transaction/memory.rs +++ b/miden-lib/src/transaction/memory.rs @@ -289,7 +289,7 @@ pub const INPUT_NOTE_DATA_SECTION_OFFSET: MemoryAddress = 1_064_960; /// The memory address at which the number of input notes is stored. pub const NUM_INPUT_NOTES_PTR: MemoryAddress = INPUT_NOTE_SECTION_OFFSET; -/// The offsets at which data of a input note is stored relative to the start of its data segment. +/// The offsets at which data of an input note is stored relative to the start of its data segment. pub const INPUT_NOTE_ID_OFFSET: MemoryOffset = 0; pub const INPUT_NOTE_SERIAL_NUM_OFFSET: MemoryOffset = 1; pub const INPUT_NOTE_SCRIPT_ROOT_OFFSET: MemoryOffset = 2; @@ -327,7 +327,7 @@ pub const OUTPUT_NOTE_SECTION_OFFSET: MemoryOffset = 4_194_304; /// The size of the core output note data segment. pub const OUTPUT_NOTE_CORE_DATA_SIZE: MemSize = 4; -/// The offsets at which data of a output note is stored relative to the start of its data segment. +/// The offsets at which data of an output note is stored relative to the start of its data segment. pub const OUTPUT_NOTE_ID_OFFSET: MemoryOffset = 0; pub const OUTPUT_NOTE_METADATA_OFFSET: MemoryOffset = 1; pub const OUTPUT_NOTE_RECIPIENT_OFFSET: MemoryOffset = 2; diff --git a/objects/src/assets/mod.rs b/objects/src/assets/mod.rs index fdf215843..64162a06b 100644 --- a/objects/src/assets/mod.rs +++ b/objects/src/assets/mod.rs @@ -211,7 +211,7 @@ impl Deserializable for Asset { /// Returns `true` if asset in [Word] is not a non-fungible asset. /// -/// Note: this does not mean that the word is a fungible asset as the word may contain an value +/// Note: this does not mean that the word is a fungible asset as the word may contain a value /// which is not a valid asset. fn is_not_a_non_fungible_asset(asset: Word) -> bool { // For fungible assets, the position `3` contains the faucet's account id, in which case the From bd34926d21cc5222d5757e87e1aaa3bd5999817c Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Tue, 24 Dec 2024 14:07:22 -0800 Subject: [PATCH 3/4] chore: fix small typos --- miden-lib/asm/kernels/transaction/api.masm | 2 +- miden-lib/asm/kernels/transaction/lib/memory.masm | 2 +- miden-lib/asm/kernels/transaction/lib/prologue.masm | 8 ++++---- miden-lib/asm/kernels/transaction/lib/tx.masm | 2 +- miden-lib/asm/kernels/transaction/main.masm | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/miden-lib/asm/kernels/transaction/api.masm b/miden-lib/asm/kernels/transaction/api.masm index eb4b149b4..13f48ee7c 100644 --- a/miden-lib/asm/kernels/transaction/api.masm +++ b/miden-lib/asm/kernels/transaction/api.masm @@ -690,7 +690,7 @@ end #! Outputs: [note_idx, ASSET, pad(11)] #! #! Where: -#! - note_idx is the index of the the note to which the asset is added. +#! - note_idx is the index of the note to which the asset is added. #! - ASSET can be a fungible or non-fungible asset. #! #! Panics if: diff --git a/miden-lib/asm/kernels/transaction/lib/memory.masm b/miden-lib/asm/kernels/transaction/lib/memory.masm index 8083b4da0..513d04953 100644 --- a/miden-lib/asm/kernels/transaction/lib/memory.masm +++ b/miden-lib/asm/kernels/transaction/lib/memory.masm @@ -1393,7 +1393,7 @@ end # KERNEL DATA # ------------------------------------------------------------------------------------------------- -#! Sets the number of of the procedures of the selected kernel. +#! Sets the number of the procedures of the selected kernel. #! #! Inputs: [num_kernel_procedures] #! Outputs: [] diff --git a/miden-lib/asm/kernels/transaction/lib/prologue.masm b/miden-lib/asm/kernels/transaction/lib/prologue.masm index 336db9b17..d3305a4bf 100644 --- a/miden-lib/asm/kernels/transaction/lib/prologue.masm +++ b/miden-lib/asm/kernels/transaction/lib/prologue.masm @@ -806,7 +806,7 @@ end #! #! Where: #! - note_ptr is the memory location for the input note. -#! - NOTE_ID is the the note's id, i.e. `hash(RECIPIENT || ASSET_HASH)`. +#! - NOTE_ID is the note's id, i.e. `hash(RECIPIENT || ASSET_HASH)`. proc.compute_input_note_id # compute SERIAL_HASH: hash(SERIAL_NUMBER || EMPTY_WORD) dup exec.memory::get_input_note_serial_num padw hmerge @@ -869,8 +869,8 @@ end #! - is_authenticated is the boolean indicating if the note contains an authentication proof. #! - optional values, required if `is_authenticated` is true: #! - block_num is the note's creation block number. -#! - BLOCK_SUB_HASH is the the block's sub_hash for which the note was created. -#! - NOTE_ROOT is the the merkle root of the note's tree. +#! - BLOCK_SUB_HASH is the block's sub_hash for which the note was created. +#! - NOTE_ROOT is the merkle root of the note's tree. proc.process_input_note # note details # --------------------------------------------------------------------------------------------- @@ -1058,7 +1058,7 @@ end #! Advice stack: [] #! #! Where: -#! - TX_SCRIPT_ROOT is the the transaction's script root. +#! - TX_SCRIPT_ROOT is the transaction's script root. proc.process_tx_script_root # read the transaction script root from the advice stack adv_loadw diff --git a/miden-lib/asm/kernels/transaction/lib/tx.masm b/miden-lib/asm/kernels/transaction/lib/tx.masm index 87245bf51..71fe80173 100644 --- a/miden-lib/asm/kernels/transaction/lib/tx.masm +++ b/miden-lib/asm/kernels/transaction/lib/tx.masm @@ -451,7 +451,7 @@ end #! Outputs: [note_idx] #! #! Where: -#! - note_idx is the index of the the note to which the asset is added. +#! - note_idx is the index of the note to which the asset is added. #! - ASSET can be a fungible or non-fungible asset. #! #! Panics if: diff --git a/miden-lib/asm/kernels/transaction/main.masm b/miden-lib/asm/kernels/transaction/main.masm index 1eead22db..9e065bcec 100644 --- a/miden-lib/asm/kernels/transaction/main.masm +++ b/miden-lib/asm/kernels/transaction/main.masm @@ -53,7 +53,7 @@ const.EPILOGUE_END=131081 #! #! Where: #! - BLOCK_HASH is the reference block for the transaction execution. -#! - account_id is the the account that the transaction is being executed against. +#! - account_id is the account that the transaction is being executed against. #! - INITIAL_ACCOUNT_HASH is the account state prior to the transaction, EMPTY_WORD for new accounts. #! - INPUT_NOTES_COMMITMENT, see `transaction::api::get_input_notes_commitment`. #! - OUTPUT_NOTES_COMMITMENT is the commitment to the notes created by the transaction. From 64d7a2e5fa3fb1c65269369bf0b152057e45befd Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 25 Dec 2024 19:22:07 +0100 Subject: [PATCH 4/4] refactor: extend `AccountId` to two felts and refactor creation process (#982) --- CHANGELOG.md | 1 + Cargo.lock | 5 + Makefile | 7 +- bin/bench-tx/src/utils.rs | 5 +- bin/tx-prover/src/main.rs | 6 +- miden-lib/asm/kernels/transaction/api.masm | 61 +- .../asm/kernels/transaction/lib/account.masm | 386 ++++-- .../asm/kernels/transaction/lib/asset.masm | 64 +- .../kernels/transaction/lib/asset_vault.masm | 175 ++- .../asm/kernels/transaction/lib/faucet.masm | 3 +- .../asm/kernels/transaction/lib/memory.masm | 78 +- .../asm/kernels/transaction/lib/note.masm | 6 +- .../asm/kernels/transaction/lib/prologue.masm | 38 +- miden-lib/asm/kernels/transaction/lib/tx.masm | 77 +- miden-lib/asm/miden/account.masm | 61 +- miden-lib/asm/miden/asset.masm | 40 +- miden-lib/asm/miden/contracts/auth/basic.masm | 6 +- miden-lib/asm/miden/note.masm | 10 +- miden-lib/asm/miden/tx.masm | 12 +- miden-lib/asm/note_scripts/P2ID.masm | 14 +- miden-lib/asm/note_scripts/P2IDR.masm | 25 +- miden-lib/src/accounts/faucets/mod.rs | 16 +- miden-lib/src/accounts/wallets/mod.rs | 26 +- miden-lib/src/errors/tx_kernel_errors.rs | 34 +- miden-lib/src/notes/mod.rs | 10 +- miden-lib/src/notes/utils.rs | 97 +- miden-lib/src/transaction/inputs.rs | 11 +- miden-lib/src/transaction/memory.rs | 3 +- miden-lib/src/transaction/mod.rs | 13 +- miden-lib/src/transaction/outputs.rs | 8 +- .../src/transaction/procedures/kernel_v0.rs | 28 +- miden-tx/Cargo.toml | 1 + miden-tx/src/executor/mod.rs | 11 +- miden-tx/src/host/mod.rs | 2 +- miden-tx/src/testing/mock_chain/mod.rs | 27 +- miden-tx/src/testing/mock_host.rs | 26 +- miden-tx/src/testing/tx_context/builder.rs | 18 +- miden-tx/src/testing/tx_context/mod.rs | 3 +- miden-tx/src/tests/kernel_tests/mod.rs | 2 +- .../src/tests/kernel_tests/test_account.rs | 52 +- miden-tx/src/tests/kernel_tests/test_asset.rs | 8 +- .../tests/kernel_tests/test_asset_vault.rs | 53 +- .../src/tests/kernel_tests/test_faucet.rs | 69 +- miden-tx/src/tests/kernel_tests/test_note.rs | 7 +- .../src/tests/kernel_tests/test_prologue.rs | 298 +++-- miden-tx/src/tests/kernel_tests/test_tx.rs | 91 +- miden-tx/src/tests/mod.rs | 33 +- miden-tx/tests/integration/main.rs | 3 +- miden-tx/tests/integration/scripts/p2id.rs | 34 +- miden-tx/tests/integration/wallet/mod.rs | 18 +- objects/Cargo.toml | 1 + objects/benches/account_seed.rs | 10 +- objects/src/accounts/account_id.rs | 1105 ++++++++++------- objects/src/accounts/account_id_anchor.rs | 118 ++ objects/src/accounts/account_id_prefix.rs | 263 ++++ objects/src/accounts/builder/mod.rs | 92 +- objects/src/accounts/data.rs | 6 +- objects/src/accounts/delta/mod.rs | 21 +- objects/src/accounts/delta/vault.rs | 16 +- objects/src/accounts/header.rs | 6 +- objects/src/accounts/mod.rs | 46 +- objects/src/accounts/seed.rs | 121 +- objects/src/assets/fungible.rs | 70 +- objects/src/assets/mod.rs | 117 +- objects/src/assets/nonfungible.rs | 139 ++- objects/src/assets/vault.rs | 11 +- objects/src/block/header.rs | 30 + objects/src/block/mod.rs | 5 +- objects/src/errors.rs | 22 +- objects/src/notes/assets.rs | 7 +- objects/src/notes/execution_hint.rs | 119 +- objects/src/notes/file.rs | 21 +- objects/src/notes/metadata.rs | 254 +++- objects/src/notes/mod.rs | 2 +- objects/src/notes/note_tag.rs | 182 ++- objects/src/testing/account.rs | 17 +- objects/src/testing/account_id.rs | 122 ++ objects/src/testing/assets.rs | 21 +- objects/src/testing/block.rs | 2 +- objects/src/testing/mod.rs | 1 + objects/src/testing/storage.rs | 33 +- objects/src/transaction/inputs.rs | 38 +- objects/src/transaction/proven_tx.rs | 8 +- 83 files changed, 3422 insertions(+), 1686 deletions(-) create mode 100644 objects/src/accounts/account_id_anchor.rs create mode 100644 objects/src/accounts/account_id_prefix.rs create mode 100644 objects/src/testing/account_id.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c94b0fa..c7fdf4ba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Added conversion from `Account` to `AccountDelta` for initial account state representation as delta (#983). - [BREAKING] Added `miden::note::get_script_hash` procedure (#995). - [BREAKING] Refactor error messages in `miden-lib` and `miden-tx` and use `thiserror` 2.0 (#1005). +- [BREAKING] Extend `AccountId` to two `Felt`s and require block hash in derivation (#982). - Removed workers list from the proxy configuration file (#1018). ## 0.6.2 (2024-11-20) diff --git a/Cargo.lock b/Cargo.lock index ae5a66c47..6483e17ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,9 @@ name = "anyhow" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +dependencies = [ + "backtrace", +] [[package]] name = "arc-swap" @@ -1918,6 +1921,7 @@ dependencies = [ name = "miden-objects" version = "0.7.0" dependencies = [ + "anyhow", "assert_matches", "criterion", "getrandom", @@ -1993,6 +1997,7 @@ dependencies = [ name = "miden-tx" version = "0.7.0" dependencies = [ + "anyhow", "assert_matches", "async-trait", "miden-assembly", diff --git a/Makefile b/Makefile index 168c1f8ae..1ab0dec4c 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ WARNINGS=RUSTDOCFLAGS="-D warnings" DEBUG_ASSERTIONS=RUSTFLAGS="-C debug-assertions" ALL_FEATURES_BUT_ASYNC=--features concurrent,testing BUILD_KERNEL_ERRORS=BUILD_KERNEL_ERRORS=1 +# Enable backtraces for tests where we return an anyhow::Result. If enabled, anyhow::Error will +# then contain a `Backtrace` and print it when a test returns an error. +BACKTRACE=RUST_BACKTRACE=1 # -- linting -------------------------------------------------------------------------------------- @@ -56,12 +59,12 @@ test-build: ## Build the test binary .PHONY: test-default test-default: ## Run default tests excluding `prove` - $(DEBUG_ASSERTIONS) cargo nextest run --profile default --cargo-profile test-release --features concurrent,testing --filter-expr "not test(prove)" + $(DEBUG_ASSERTIONS) $(BACKTRACE) cargo nextest run --profile default --cargo-profile test-release --features concurrent,testing --filter-expr "not test(prove)" .PHONY: test-prove test-prove: ## Run `prove` tests (tests which use the Miden prover) - $(DEBUG_ASSERTIONS) cargo nextest run --profile prove --cargo-profile test-release --features concurrent,testing --filter-expr "test(prove)" + $(DEBUG_ASSERTIONS) $(BACKTRACE) cargo nextest run --profile prove --cargo-profile test-release --features concurrent,testing --filter-expr "test(prove)" .PHONY: test diff --git a/bin/bench-tx/src/utils.rs b/bin/bench-tx/src/utils.rs index 010762780..755a8446a 100644 --- a/bin/bench-tx/src/utils.rs +++ b/bin/bench-tx/src/utils.rs @@ -21,8 +21,9 @@ use super::{read_to_string, write, Benchmark, Path}; // CONSTANTS // ================================================================================================ -pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN: u64 = 0x200000000000001f; // 2305843009213693983 -pub const ACCOUNT_ID_SENDER: u64 = 0x800000000000001f; // 9223372036854775839 +// Copied from miden_objects::testing::account_id. +pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN: u128 = 0x00aa00000000bc200000bc000000de00; +pub const ACCOUNT_ID_SENDER: u128 = 0x00fa00000000bb800000cc000000de00; pub const DEFAULT_AUTH_SCRIPT: &str = " begin diff --git a/bin/tx-prover/src/main.rs b/bin/tx-prover/src/main.rs index 176a353cf..a9ba6a088 100644 --- a/bin/tx-prover/src/main.rs +++ b/bin/tx-prover/src/main.rs @@ -27,10 +27,12 @@ mod test { use miden_lib::transaction::TransactionKernel; use miden_objects::{ - accounts::account_id::testing::{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_SENDER}, assets::{Asset, FungibleAsset}, notes::NoteType, - testing::account_code::DEFAULT_AUTH_SCRIPT, + testing::{ + account_code::DEFAULT_AUTH_SCRIPT, + account_id::{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_SENDER}, + }, transaction::{ProvenTransaction, TransactionScript, TransactionWitness}, }; use miden_tx::{ diff --git a/miden-lib/asm/kernels/transaction/api.masm b/miden-lib/asm/kernels/transaction/api.masm index 13f48ee7c..a7b978fe8 100644 --- a/miden-lib/asm/kernels/transaction/api.masm +++ b/miden-lib/asm/kernels/transaction/api.masm @@ -74,7 +74,7 @@ end #! Returns the account id. #! #! Inputs: [pad(16)] -#! Outputs: [acct_id, pad(15)] +#! Outputs: [acct_id_hi, acct_id_lo, pad(14)] #! #! Where: #! - acct_id is the account id. @@ -83,11 +83,11 @@ end export.get_account_id # get the account id exec.account::get_id - # => [acct_id, pad(16)] + # => [acct_id_hi, acct_id_lo, pad(16)] # truncate the stack - swap drop - # => [acct_id, pad(15)] + movup.2 drop movup.2 drop + # => [acct_id_hi, acct_id_lo, pad(14)] end #! Returns the account nonce. @@ -229,7 +229,7 @@ export.set_account_item # if the transaction is being executed against a faucet account then assert # index != FAUCET_STORAGE_DATA_SLOT (reserved slot) dup exec.account::get_faucet_storage_data_slot eq - exec.account::get_id exec.account::is_faucet + exec.account::get_id swap drop exec.account::is_faucet and assertz.err=ERR_FAUCET_STORAGE_DATA_SLOT_IS_RESERVED # => [index, V', pad(11)] @@ -371,11 +371,12 @@ end #! Returns the balance of a fungible asset associated with a faucet_id. #! -#! Inputs: [faucet_id, pad(15)] +#! Inputs: [faucet_id_hi, faucet_id_lo, pad(14)] #! Outputs: [balance, pad(15)] #! #! Where: -#! - faucet_id is the faucet id of the fungible asset of interest. +#! - faucet_id_{hi,lo} are the first and second felt of the faucet id of the fungible asset +#! of interest. #! - balance is the vault balance of the fungible asset. #! #! Panics if: @@ -384,8 +385,8 @@ end #! Invocation: dynexec export.account_vault_get_balance # get the vault root - exec.memory::get_acct_vault_root_ptr swap - # => [faucet_id, acct_vault_root_ptr, pad(15)] + exec.memory::get_acct_vault_root_ptr movdn.2 + # => [faucet_id_hi, faucet_id_lo, acct_vault_root_ptr, pad(13)] # get the asset balance exec.asset_vault::get_balance @@ -554,22 +555,23 @@ end #! Returns the sender of the note currently being processed. #! #! Inputs: [pad(16)] -#! Outputs: [sender, pad(15)] +#! Outputs: [sender_hi, sender_lo, pad(14)] #! #! Where: -#! - sender is the sender of the note currently being processed. +#! - sender_{hi,lo} are the first and second felt of the sender account id of the note currently +#! being processed. #! #! Panics if: #! - a note is not being processed. #! #! Invocation: dynexec export.get_note_sender - exec.note::get_sender - # => [sender, pad(16)] + exec.note::get_sender + # => [sender_hi, sender_lo, pad(16)] # truncate the stack - swap drop - # => [sender, pad(15)] + swapw dropw + # => [sender_hi, sender_lo, pad(14)] end #! Returns the block number of the last known block at the time of transaction execution. @@ -818,7 +820,7 @@ end #! Invocation: dynexec export.get_fungible_faucet_total_issuance # assert that we are executing a transaction against a fungible faucet (access checks) - exec.account::get_id exec.account::is_fungible_faucet + exec.account::get_id swap drop exec.account::is_fungible_faucet assert.err=ERR_ACCOUNT_TOTAL_ISSUANCE_PROC_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_FAUCET # => [pad(16)] @@ -879,10 +881,10 @@ end #! Moves the account pointer to the currently accessing foreign account. #! #! Inputs: -#! Operand stack: [foreign_account_id, pad(15)] +#! Operand stack: [foreign_account_id_hi, foreign_account_id_lo, pad(14)] #! Advice map: { -#! FOREIGN_ACCOUNT_ID: [[foreign_account_id, 0, 0, account_nonce], VAULT_ROOT, STORAGE_ROOT, -#! CODE_ROOT], +#! FOREIGN_ACCOUNT_ID: [[foreign_account_lo, foreign_account_id_hi, 0, account_nonce], +#! VAULT_ROOT, STORAGE_ROOT, CODE_ROOT], #! STORAGE_ROOT: [[STORAGE_SLOT_DATA]], #! CODE_ROOT: [num_procs, [ACCOUNT_PROCEDURE_DATA]] #! } @@ -890,9 +892,10 @@ end #! Operand stack: [pad(16)] #! #! Where: -#! - foreign_account_id is the ID of the foreign account whose procedure is going to be executed. +#! - foreign_account_id_{hi,lo} are the first and second felt of the ID of the foreign account +#! whose procedure is going to be executed. #! - FOREIGN_ACCOUNT_ID is the word constructed from the foreign_account_id as follows: -#! [foreign_account_id, 0, 0, 0]. +#! [foreign_account_id_hi, foreign_account_lo, 0, 0]. #! - account_nonce is the nonce of the foreign account. #! - VAULT_ROOT is the commitment of the foreign account's vault. #! - STORAGE_ROOT is the commitment of the foreign account's storage. @@ -909,27 +912,27 @@ end export.start_foreign_context # check that this procedure was executed against the native account exec.memory::assert_native_account - # OS => [foreign_account_id, pad(15)] + # OS => [foreign_account_id_hi, foreign_account_id_lo, pad(14)] # get the memory address and a flag whether this account was already loaded. exec.account::get_foreign_account_ptr - # OS => [was_loaded, ptr, foreign_account_id, pad(15)] + # OS => [was_loaded, ptr, foreign_account_id_hi, foreign_account_id_lo, pad(14)] if.true - exec.memory::set_current_account_data_ptr drop + exec.memory::set_current_account_data_ptr drop drop # OS => [pad(16)] else exec.memory::set_current_account_data_ptr - # OS => [foreign_account_id, pad(15)] + # OS => [foreign_account_id_hi, foreign_account_id_lo, pad(14)] # construct the word with account ID to load the core account data from the advice map - push.0.0.0 - # OS => [0, 0, 0, foreign_account_id, pad(15)] + swap push.0.0 + # OS => [0, 0, foreign_account_id_lo, foreign_account_id_hi, pad(14)] # move the core account data to the advice stack adv.push_mapval - # OS => [0, 0, 0, foreign_account_id, pad(15)] - # AS => [[foreign_account_id, 0, 0, account_nonce], VAULT_ROOT, STORAGE_ROOT, CODE_ROOT] + # OS => [0, 0, foreign_account_id_lo, foreign_account_id_hi, pad(14)] + # AS => [[foreign_account_id_hi, foreign_account_lo, 0, account_nonce], VAULT_ROOT, STORAGE_ROOT, CODE_ROOT] # store the id and nonce of the foreign account to the memory dropw adv_loadw diff --git a/miden-lib/asm/kernels/transaction/lib/account.masm b/miden-lib/asm/kernels/transaction/lib/account.masm index e0e29b956..1dd46e278 100644 --- a/miden-lib/asm/kernels/transaction/lib/account.masm +++ b/miden-lib/asm/kernels/transaction/lib/account.masm @@ -1,3 +1,4 @@ +use.std::collections::mmr use.std::collections::smt use.std::crypto::hashes::rpo use.std::mem @@ -11,17 +12,17 @@ use.kernel::memory # Account nonce cannot be increased by a greater than u32 value const.ERR_ACCOUNT_NONCE_INCREASE_MUST_BE_U32=0x00020004 -# Account ID must contain at least MIN_ACCOUNT_ONES number of ones -const.ERR_ACCOUNT_INSUFFICIENT_NUMBER_OF_ONES=0x00020005 +# Least significant byte of second felt of the account id must be zero. +const.ERR_ACCOUNT_ID_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO=0x00020005 # Account code must be updatable for it to be possible to set new code const.ERR_ACCOUNT_CODE_IS_NOT_UPDATABLE=0x00020006 -# ID of the new account does not match the ID computed from the seed -const.ERR_ACCOUNT_SEED_DIGEST_MISMATCH=0x00020007 +# Anchor block hash must not be empty +const.ERR_ACCOUNT_ANCHOR_BLOCK_HASH_MUST_NOT_BE_EMPTY=0x00020007 -# Account proof of work is insufficient -const.ERR_ACCOUNT_POW_IS_INSUFFICIENT=0x00020008 +# ID of the new account does not match the ID computed from the seed and anchor block hash +const.ERR_ACCOUNT_SEED_ANCHOR_BLOCK_HASH_DIGEST_MISMATCH=0x00020008 # Failed to write an account value item to a non-value storage slot const.ERR_ACCOUNT_SETTING_VALUE_ITEM_ON_NON_VALUE_SLOT=0x00020009 @@ -68,31 +69,38 @@ const.ERR_FOREIGN_ACCOUNT_ID_EQUALS_NATIVE_ACCT_ID=0x00020016 # State of the current foreign account is invalid. const.ERR_FOREIGN_ACCOUNT_INVALID=0x00020017 +# Unknown version in account id. +const.ERR_ACCOUNT_ID_UNKNOWN_VERSION=0x00020057 + +# Epoch must be less than u16::MAX (0xffff). +const.ERR_ACCOUNT_ID_EPOCH_MUST_BE_LESS_THAN_U16_MAX=0x00020058 + # CONSTANTS # ================================================================================================= -# Given the most significant half of an account id, this mask defines the bits used to determine the -# account type. -const.ACCOUNT_TYPE_U32MASK=805306368 # 0b00110000_00000000_00000000_00000000 +# Given the least significant 32 bits of an account id's first felt, this mask defines the bits used +# to determine the account version. +const.ACCOUNT_VERSION_MASK_U32=0x0f # 0b1111 + +# Given the least significant 32 bits of an account id's first felt, this mask defines the bits used +# to determine the account type. +const.ACCOUNT_ID_TYPE_MASK_U32=0x30 # 0b11_0000 # Bit pattern for an account w/ immutable code, after the account type mask has been applied. -const.REGULAR_ACCOUNT_IMMUTABLE_CODE=0 # 0b00000000_00000000_00000000_00000000 +const.REGULAR_ACCOUNT_IMMUTABLE_CODE=0 # 0b00_0000 # Bit pattern for an account w/ updatable code, after the account type mask has been applied. -const.REGULAR_ACCOUNT_UPDATABLE_CODE=268435456 # 0b00010000_00000000_00000000_00000000 +const.REGULAR_ACCOUNT_UPDATABLE_CODE=0x10 # 0b01_0000 # Bit pattern for a fungible faucet w/ immutable code, after the account type mask has been applied. -const.FUNGIBLE_FAUCET_ACCOUNT=536870912 # 0b00100000_00000000_00000000_00000000 +const.FUNGIBLE_FAUCET_ACCOUNT=0x20 # 0b10_0000 # Bit pattern for a non-fungible faucet w/ immutable code, after the account type mask has been # applied. -const.NON_FUNGIBLE_FAUCET_ACCOUNT=805306368 # 0b00110000_00000000_00000000_00000000 +const.NON_FUNGIBLE_FAUCET_ACCOUNT=0x30 # 0b11_0000 # Bit pattern for a faucet account, after the account type mask has been applied. -const.FAUCET_ACCOUNT=536870912 # 0b00100000_00000000_00000000_00000000 - -# Specifies a minimum number of ones for a valid account ID. -const.MIN_ACCOUNT_ONES=5 +const.FAUCET_ACCOUNT=0x20 # 0b10_0000 # The maximum number of account interface procedures. const.MAX_NUM_PROCEDURES=256 @@ -253,11 +261,11 @@ export.memory::get_init_acct_hash->get_initial_hash #! Returns a boolean indicating whether the account is a fungible faucet. #! -#! Inputs: [acct_id] +#! Inputs: [acct_id_hi] #! Outputs: [is_fungible_faucet] #! #! Where: -#! - acct_id is the account id. +#! - acct_id_hi is the first felt of the account id. #! - is_fungible_faucet is a boolean indicating whether the account is a fungible faucet. export.is_fungible_faucet exec.type push.FUNGIBLE_FAUCET_ACCOUNT eq @@ -266,11 +274,11 @@ end #! Returns a boolean indicating whether the account is a non-fungible faucet. #! -#! Inputs: [acct_id] +#! Inputs: [acct_id_hi] #! Outputs: [is_non_fungible_faucet] #! #! Where: -#! - acct_id is the account id. +#! - acct_id_hi is the first felt of the account id. #! - is_non_fungible_faucet is a boolean indicating whether the account is a non-fungible faucet. export.is_non_fungible_faucet exec.type push.NON_FUNGIBLE_FAUCET_ACCOUNT eq @@ -279,24 +287,24 @@ end #! Returns a boolean indicating whether the account is a faucet. #! -#! Inputs: [acct_id] +#! Inputs: [acct_id_hi] #! Outputs: [is_faucet] #! #! Where: -#! - acct_id is the account id. +#! - acct_id_hi is the first felt of the account id. #! - is_faucet is a boolean indicating whether the account is a faucet. export.is_faucet - u32split swap drop push.FAUCET_ACCOUNT u32and eq.0 not + u32split drop push.FAUCET_ACCOUNT u32and eq.0 not # => [is_faucet] end #! Returns a boolean indicating whether the account is a regular updatable account. #! -#! Inputs: [acct_id] +#! Inputs: [acct_id_hi] #! Outputs: [is_updatable_account] #! #! Where: -#! - acct_id is the account id. +#! - acct_id_hi is the first felt of the account id. #! - is_updatable_account is a boolean indicating whether the account is a regular updatable #! account. export.is_updatable_account @@ -306,11 +314,11 @@ end #! Returns a boolean indicating whether the account is a regular immutable account. #! -#! Inputs: [acct_id] +#! Inputs: [acct_id_hi] #! Outputs: [is_immutable_account] #! #! Where: -#! - acct_id is the account id. +#! - acct_id_hi is the first felt of the account id. #! - is_immutable_account is a boolean indicating whether the account is a regular immutable #! account. export.is_immutable_account @@ -318,27 +326,60 @@ export.is_immutable_account # => [is_immutable_account] end +#! Returns a boolean indicating whether the given account_ids are equal. +#! +#! Inputs: [acct_id_hi, acct_id_lo, other_acct_id_hi, other_acct_id_lo] +#! Outputs: [is_id_equal] +#! +#! Where: +#! - acct_id_{hi,lo} are the first and second felt of an account id. +#! - other_acct_id_{hi,lo} are the first and second felt of the other account id to compare against. +#! - is_id_equal is a boolean indicating whether the account ids are equal. +export.is_id_eq + movup.2 eq + # => [is_hi_equal, acct_id_lo, other_acct_id_lo] + movdn.2 eq + # => [is_lo_equal, is_hi_equal] + and + # => [is_id_equal] +end + #! Validates an account id. #! -#! Inputs: [acct_id] +#! Inputs: [account_id_hi, account_id_lo] #! Outputs: [] #! #! Where: -#! - acct_id is the account id. +#! - account_id_{hi,lo} are the first and second felt of the account id. #! #! Panics if: -#! - the account id is invalid: account id must have at least `MIN_ACCOUNT_ONES` ones. +#! - account_id_hi does not contain version zero. +#! - account_id_lo contains an anchor epoch that is greater or equal to 2^16. +#! - account_id_lo does not have its lower 8 bits set to zero. export.validate_id - # split felt into 32 bit limbs - u32split - # => [l_1, l_0] + # Validate version in first felt. For now only version 0 is supported. + # --------------------------------------------------------------------------------------------- + + exec.id_version + # => [id_version, account_id_lo] + assertz.err=ERR_ACCOUNT_ID_UNKNOWN_VERSION + # => [account_id_lo] + + # Validate anchor epoch is less than u16::MAX (0xffff) in second felt. + # --------------------------------------------------------------------------------------------- - # count the number of 1 bits - u32popcnt swap u32popcnt add - # => [ones] + dup exec.id_anchor_epoch + # => [anchor_epoch, account_id_lo] + lt.0xffff assert.err=ERR_ACCOUNT_ID_EPOCH_MUST_BE_LESS_THAN_U16_MAX + # => [account_id_lo] - # check if the number of ones is at least MIN_ACCOUNT_ONES ones. - push.MIN_ACCOUNT_ONES u32gte assert.err=ERR_ACCOUNT_INSUFFICIENT_NUMBER_OF_ONES + # Validate lower 8 bits of second felt are zero. + # --------------------------------------------------------------------------------------------- + + u32split drop u32and.0xff eq.0 + # => [is_least_significant_byte_zero] + assert.err=ERR_ACCOUNT_ID_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO + # => [] end #! Sets the code of the account the transaction is being executed against. @@ -353,8 +394,8 @@ end #! - this procedure is executed on an account whose type differs from `regular mutable`. export.set_code # get the account id - exec.memory::get_account_id - # => [acct_id, CODE_COMMITMENT] + exec.memory::get_account_id swap drop + # => [acct_id_hi, CODE_COMMITMENT] # assert the account is an updatable regular account exec.is_updatable_account assert.err=ERR_ACCOUNT_CODE_IS_NOT_UPDATABLE @@ -406,7 +447,7 @@ export.validate_procedure_metadata # => [start_loop, index, num_storage_slots, num_account_procedures] # check if the account is a faucet - exec.get_id exec.is_faucet + exec.get_id swap drop exec.is_faucet # => [is_faucet, start_loop, index, num_storage_slots, num_account_procedures] # we do not check if num_account_procedures == 0 here because a valid @@ -525,7 +566,7 @@ export.set_item # => [storage_slot_type, index, V'] # check if type == slot - exec.constants::get_storage_slot_type_value eq + exec.constants::get_storage_slot_type_value eq assert.err=ERR_ACCOUNT_SETTING_VALUE_ITEM_ON_NON_VALUE_SLOT # => [index, V'] @@ -695,7 +736,7 @@ end #! Validates that the account seed, provided via the advice map, satisfies the seed requirements. #! #! Validation is performed via the following steps: -#! 1. Compute the hash of (SEED, CODE_COMMITMENT, STORAGE_COMMITMENT, 0, 0, 0, 0). +#! 1. Compute the hash of (SEED, CODE_COMMITMENT, STORAGE_COMMITMENT, ANCHOR_BLOCK_HASH). #! 2. Assert the least significant element of the digest is equal to the account id of the account #! the transaction is being executed against. #! 3. Assert the most significant element has sufficient proof of work (trailing zeros) for the @@ -704,59 +745,185 @@ end #! Inputs: [] #! Outputs: [] export.validate_seed - # pad capacity elements of hasher and populate first four elements of the rate with the account - # id seed - padw exec.memory::get_account_id push.0.0.0 + # Load the block hash of the anchor block from the chain mmr. + # This is the block hash to which the account ID is anchored and is derived from. + # --------------------------------------------------------------------------------------------- + + # prepare the advice map key for the seed for later + exec.memory::get_account_id push.0.0 + # => [0, 0, account_id_hi, account_id_lo] + + # get the anchor block's number + dup.3 exec.id_anchor_block_num + # => [anchor_block_num, 0, 0, account_id_hi, account_id_lo] + + exec.memory::get_chain_mmr_ptr swap + # => [anchor_block_num, chain_mmr_ptr, 0, 0, account_id_hi, account_id_lo] + + exec.mmr::get + # => [ANCHOR_BLOCK_HASH, 0, 0, account_id_hi, account_id_lo] + + # assert that the anchor block hash is not the empty word + padw eqw not assert.err=ERR_ACCOUNT_ANCHOR_BLOCK_HASH_MUST_NOT_BE_EMPTY dropw + # => [ANCHOR_BLOCK_HASH, 0, 0, account_id_hi, account_id_lo] + + # Compute the hash of (SEED, CODE_COMMITMENT, STORAGE_COMMITMENT, ANCHOR_BLOCK_HASH). + # --------------------------------------------------------------------------------------------- + + # prepare advice push_mapval + swapw + # => [0, 0, account_id_hi, account_id_lo, ANCHOR_BLOCK_HASH] + + # populate first four elements of the rate with the account id seed adv.push_mapval push.15263 drop # TODO: remove line, see miden-vm/#1122 adv_loadw - # => [SEED, 0, 0, 0, 0] + # => [SEED, ANCHOR_BLOCK_HASH] + + # pad capacity element of hasher + padw swapw + # => [SEED, 0, 0, 0, 0, ANCHOR_BLOCK_HASH] # populate last four elements of the hasher rate with the code commitment exec.memory::get_acct_code_commitment - # => [CODE_COMMITMENT, SEED, 0, 0, 0, 0] + # => [CODE_COMMITMENT, SEED, 0, 0, 0, 0, ANCHOR_BLOCK_HASH] # perform first permutation of seed and code_commitment (from advice stack) # perm(seed, code_commitment) hperm - # => [RATE, RATE, PERM] + # => [RATE, RATE, PERM, ANCHOR_BLOCK_HASH] # clear rate elements dropw dropw - # => [PERM] + # => [PERM, ANCHOR_BLOCK_HASH] # perform second permutation perm(storage_commitment, 0, 0, 0, 0) - exec.memory::get_acct_storage_commitment padw hperm + swapw exec.memory::get_acct_storage_commitment swapw + # => [ANCHOR_BLOCK_HASH, STORAGE_COMMITMENT, PERM] + + hperm # => [RATE, RATE, CAP] # extract digest exec.rpo::squeeze_digest - # => [DIG] + # => [DIGEST] - # assert the account id matches the account id of the new account and extract pow - # element - movdn.3 drop drop exec.memory::get_account_id eq assert.err=ERR_ACCOUNT_SEED_DIGEST_MISMATCH - # => [pow] + # Shape second felt to add the anchor epoch and compare computed and provided ID. + # --------------------------------------------------------------------------------------------- - # get acct and faucet modulus to check the min number of trailing zeros required in the pow - exec.constants::get_regular_account_seed_digest_modulus - exec.constants::get_faucet_seed_digest_modulus - # => [faucet_modulus, acct_modulus, pow] + # extract account id from digest + drop drop swap + # => [hashed_account_id_hi, hashed_account_id_lo] - exec.memory::get_account_id - # => [acct_id, faucet_modulus, acct_modulus, pow] + exec.memory::get_account_id movdn.3 + # => [account_id_lo, hashed_account_id_hi, hashed_account_id_lo, account_id_hi] - exec.is_faucet - # => [is_faucet, faucet_modulus, acct_modulus, pow] + # extract anchor epoch from ID of the new account + dup movdn.4 exec.id_anchor_epoch + # => [anchor_epoch, hashed_account_id_hi, hashed_account_id_lo, account_id_hi, account_id_lo] - # select the appropriate modulus based on the account type - cdrop swap - # => [pow, modulus] + # shape second felt of hashed id, adding the anchor epoch and setting the lower 8 bits to zero + movup.2 exec.shape_second_felt swap + # => [hashed_account_id_hi, hashed_account_id_lo, account_id_hi, account_id_lo] - # assert that the pow is valid - u32split drop swap u32divmod assertz.err=ERR_ACCOUNT_POW_IS_INSUFFICIENT drop + # assert the account id matches the account id of the new account + exec.is_id_eq assert.err=ERR_ACCOUNT_SEED_ANCHOR_BLOCK_HASH_DIGEST_MISMATCH # => [] end +#! Shapes the second felt so it meets the requirements of the account ID, by overwriting the +#! upper 16 bits with the anchor epoch and setting the lower 8 bits to zero. +#! +#! Inputs: [seed_digest_lo, anchor_epoch] +#! Outputs: [account_id_lo] +#! +#! Where: +#! - seed_digest_lo is the second felt of the digest that should be shaped into the second felt +#! of an account ID. +#! - account_id_lo is the second felt of an account ID. +#! - anchor_epoch is the epoch number to which this account ID is anchored. +proc.shape_second_felt + u32split + # => [seed_digest_lo_hi, seed_digest_lo_lo, anchor_epoch] + + # clear epoch bits in hi part so we can set them later + u32and.0x0000ffff swap + # => [seed_digest_lo_lo, seed_digest_lo_hi', anchor_epoch] + + # clear lower 8 bits of the lo part + u32and.0xffffff00 swap.2 + # => [anchor_epoch, seed_digest_lo_hi', seed_digest_lo_lo'] + + # assert epoch is not 2^16 + # this is technically optional as we will compare this id with the provided one for which + # this property was already checked, but since this check is cheap we include it anyway + dup eq.0xffff assertz.err=ERR_ACCOUNT_ID_EPOCH_MUST_BE_LESS_THAN_U16_MAX + # => [anchor_epoch, seed_digest_lo_hi', seed_digest_lo_lo'] + + # shift epoch left by 16 bits and set epoch bits on hi part + u32shl.16 u32or + # => [seed_digest_lo_hi'', seed_digest_lo_lo'] + + # reassemble the second felt by multiplying the hi part with 2^32 and adding the lo part + mul.0x0100000000 add + # => [account_id_lo] +end + +#! Extracts the block number of the anchor block from the second felt of an account ID. +#! +#! Inputs: [account_id_lo] +#! Outputs: [anchor_block_num] +#! +#! Where: +#! - account_id_lo is the second felt of an account ID. +#! - anchor_block_num is the number of the block to which this account ID is anchored. +proc.id_anchor_block_num + # extract the upper 32 bits + u32split swap drop + # => [account_id_lo_hi] + + # to get the epoch's block number we would have to multiply the epoch in the account id by 2^16 + # since the epoch is already in the upper 16 bits of the u32, we can simply zero out the + # lower 16 bits to achieve the same result. + u32and.0xffff0000 + # => [anchor_block_num] +end + +#! Extracts the epoch from the second felt of an account ID. +#! +#! Inputs: [account_id_lo] +#! Outputs: [anchor_epoch] +#! +#! Where: +#! - account_id_lo is the second felt of an account ID. +#! - anchor_epoch is the epoch number to which this account ID is anchored. +proc.id_anchor_epoch + # extract the upper 32 bits + u32split swap drop + # => [account_id_lo_hi] + + # shift the upper 16 bits to the right to produce the epoch + u32shr.16 + # => [anchor_epoch] +end + +#! Extracts the account ID version from the first felt of an account ID. +#! +#! Inputs: [account_id_hi] +#! Outputs: [id_version] +#! +#! Where: +#! - account_id_hi is the first felt of an account ID. +#! - id_version is the version number of the ID. +proc.id_version + # extract the lower 32 bits + u32split drop + # => [account_id_hi_lo] + + # mask out the version + u32and.ACCOUNT_VERSION_MASK_U32 + # => [id_version] +end + # DATA LOADERS # ================================================================================================= @@ -904,19 +1071,19 @@ end #! Returns the most significant half with the account type bits masked out. #! #! The account type can be defined by comparing this value with the following constants: -#! - REGULAR_ACCOUNT_UPDATABLE_CODE # 0b00010000_00000000_00000000_00000000 -#! - REGULAR_ACCOUNT_IMMUTABLE_CODE # 0b00000000_00000000_00000000_00000000 -#! - FUNGIBLE_FAUCET_ACCOUNT # 0b00100000_00000000_00000000_00000000 -#! - NON_FUNGIBLE_FAUCET_ACCOUNT # 0b00110000_00000000_00000000_00000000 +#! - REGULAR_ACCOUNT_UPDATABLE_CODE +#! - REGULAR_ACCOUNT_IMMUTABLE_CODE +#! - FUNGIBLE_FAUCET_ACCOUNT +#! - NON_FUNGIBLE_FAUCET_ACCOUNT #! -#! Inputs: [acct_id] +#! Inputs: [acct_id_hi] #! Outputs: [acct_type] #! #! Where: -#! - acct_id is the account id. +#! - acct_id_hi is the first felt of the account id. #! - acct_type is the account type. proc.type - u32split swap drop push.ACCOUNT_TYPE_U32MASK u32and + u32split drop push.ACCOUNT_ID_TYPE_MASK_U32 u32and # => [acct_type] end @@ -975,73 +1142,75 @@ end #! Returns the pointer to the next vacant memory slot if the account was not loaded before, and the #! pointer to the account data otherwise. #! -#! Inputs: [foreign_account_id] -#! Outputs: [was_loaded, ptr, foreign_account_id] +#! Inputs: [foreign_account_id_hi, foreign_account_id_lo] +#! Outputs: [was_loaded, ptr, foreign_account_id_hi, foreign_account_id_lo] #! #! Where: -#! - foreign_account_id is the ID of the foreign account whose procedure is going to be executed. -#! - was_loaded is the binary flag indicating whether the foreign account was already loaded to the +#! - foreign_account_id_{hi,lo} are the first and second felt of the ID of the foreign account +#! whose procedure is going to be executed. +#! - was_loaded is the binary flag indicating whether the foreign account was already loaded to the #! memory. -#! - ptr is the memory pointer to the next empty memory slot or the memory pointer to the account +#! - ptr is the memory pointer to the next empty memory slot or the memory pointer to the account #! data, depending on the value of the was_loaded flag. #! #! Panics if: -#! - the ID of the provided foreign account equals zero. +#! - the first or second felt of the provided foreign account ID equal zero. #! - the maximum allowed number of foreign account to be loaded (64) was exceeded. export.get_foreign_account_ptr # check that foreign account id is not equal zero - dup neq.0 assert.err=ERR_FOREIGN_ACCOUNT_ID_IS_ZERO - # => [foreign_account_id] + dup.1 eq.0 dup.1 eq.0 and not assert.err=ERR_FOREIGN_ACCOUNT_ID_IS_ZERO + # => [foreign_account_id_hi, foreign_account_id_lo] # check that foreign account id is not equal to the native account id - dup exec.memory::get_native_account_id neq + dup.1 dup.1 exec.memory::get_native_account_id exec.is_id_eq not assert.err=ERR_FOREIGN_ACCOUNT_ID_EQUALS_NATIVE_ACCT_ID # get the initial account data pointer exec.memory::get_native_account_data_ptr - # => [curr_account_ptr, foreign_account_id] + # => [curr_account_ptr, foreign_account_id_hi, foreign_account_id_lo] # push the flag to enter the loop push.1 while.true # drop the flag left from the previous loop - movup.2 drop - # => [curr_account_ptr, foreign_account_id] + # in the first iteration this will be a pad element + movup.3 drop + # => [curr_account_ptr, foreign_account_id_hi, foreign_account_id_lo] # move the current account pointer to the next account data block exec.memory::get_account_data_length add - # => [curr_account_ptr', foreign_account_id] + # => [curr_account_ptr', foreign_account_id_hi, foreign_account_id_lo] # load the first data word at the current account pointer padw dup.4 mem_loadw - # => [FIRST_DATA_WORD, curr_account_ptr', foreign_account_id] + # => [FIRST_DATA_WORD, curr_account_ptr', foreign_account_id_hi, foreign_account_id_lo] - # check whether the last value in the word equals zero. If so, it will mean that this memory - # block was not initialized. - drop drop drop dup eq.0 - # => [is_empty_block, last_data_value, curr_account_ptr', foreign_account_id] + # check whether the last value in the word equals zero + # if so it means this memory block was not yet initialized + drop drop dup.1 eq.0 + # => [is_empty_block, maybe_account_id_hi, maybe_account_id_lo, curr_account_ptr', foreign_account_id_hi, foreign_account_id_lo] - # check whether the current id matches the foreign id - swap dup.3 eq - # => [is_equal_id, is_empty_word, curr_account_ptr', foreign_account_id] + # check whether the current id matches the foreign id + movdn.2 dup.5 dup.5 exec.is_id_eq + # => [is_equal_id, is_empty_word, curr_account_ptr', foreign_account_id_hi, foreign_account_id_lo] # get the loop flag # it equals 1 if both `is_equal_id` and `is_empty_block` flags are equal to 0, so we should # continue iterating - dup movdn.4 or not - # => [loop_flag, curr_account_ptr', foreign_account_id, is_equal_id] + dup movdn.5 or not + # => [loop_flag, curr_account_ptr', foreign_account_id_hi, foreign_account_id_lo, is_equal_id] end # check that the loading of one more account won't exceed the maximum number of the foreign # accounts which can be loaded. dup exec.memory::get_max_foreign_account_ptr lte assert.err=ERR_FOREIGN_ACCOUNT_MAX_NUMBER_EXCEEDED - # => [curr_account_ptr, foreign_account_id, is_equal_id] + # => [curr_account_ptr, foreign_account_id_hi, foreign_account_id_lo, is_equal_id] # the resulting `was_loaded` flag is essentially equal to the `is_equal_id` flag - movup.2 - # => [was_loaded, curr_account_ptr, foreign_account_id] + movup.3 + # => [was_loaded, curr_account_ptr, foreign_account_id_hi, foreign_account_id_lo] end #! Checks that the state of the current foreign account is valid. @@ -1056,21 +1225,22 @@ export.validate_current_foreign_account exec.memory::get_acct_db_root # => [ACCOUNT_DB_ROOT] + # TODO: Update when account tree was updated. # get the current account ID - exec.memory::get_account_id - # => [account_id, ACCOUNT_DB_ROOT] + exec.memory::get_account_id swap drop + # => [account_id_hi, ACCOUNT_DB_ROOT] # push the depth of the account database tree push.ACCOUNT_TREE_DEPTH - # => [depth, account_id, ACCOUNT_DB_ROOT] + # => [depth, account_id_hi, ACCOUNT_DB_ROOT] # get the foreign account hash exec.get_current_hash - # => [FOREIGN_ACCOUNT_HASH, depth, account_id, ACCOUNT_DB_ROOT] + # => [FOREIGN_ACCOUNT_HASH, depth, account_id_hi, ACCOUNT_DB_ROOT] # verify that the account database has the hash of the current foreign account mtree_verify.err=ERR_FOREIGN_ACCOUNT_INVALID - # => [FOREIGN_ACCOUNT_HASH, depth, account_id, ACCOUNT_DB_ROOT] + # => [FOREIGN_ACCOUNT_HASH, depth, account_id_hi, ACCOUNT_DB_ROOT] # clean the stack dropw drop drop dropw diff --git a/miden-lib/asm/kernels/transaction/lib/asset.masm b/miden-lib/asm/kernels/transaction/lib/asset.masm index 6c0834b3d..6d6add3a3 100644 --- a/miden-lib/asm/kernels/transaction/lib/asset.masm +++ b/miden-lib/asm/kernels/transaction/lib/asset.masm @@ -6,17 +6,14 @@ use.kernel::account # Malformed fungible asset: ASSET[1] must be 0 const.ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO=0x00020020 -# Malformed fungible asset: ASSET[2] must be 0 -const.ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_MUST_BE_ZERO=0x00020021 - -# Malformed fungible asset: ASSET[3] must be a valide fungible faucet id -const.ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID=0x00020022 +# Malformed fungible asset: ASSET[2] and ASSET[3] must be a valid fungible faucet id +const.ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_AND_THREE_MUST_BE_FUNGIBLE_FAUCET_ID=0x00020022 # Malformed fungible asset: ASSET[0] exceeds the maximum allowed amount const.ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS=0x00020023 -# Malformed non-fungible asset: ASSET[1] is not a valid non-fungible faucet id -const.ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_FUNGIBLE_FAUCET_ID=0x00020024 +# Malformed non-fungible asset: ASSET[3] is not a valid non-fungible faucet id +const.ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID=0x00020024 # Malformed non-fungible asset: the most significant bit must be 0 const.ERR_NON_FUNGIBLE_ASSET_FORMAT_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO=0x00020025 @@ -32,10 +29,6 @@ const.ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN=0x00020027 const.FUNGIBLE_ASSET_MAX_AMOUNT=9223372036854775807 -# This mask defines the bit in the most significant half of the element which -# is used to identify the asset type -const.FUNGIBLE_BITMASK_U32=0x20000000 - #! Returns the maximum amount of a fungible asset. #! #! Inputs: [] @@ -63,20 +56,16 @@ end #! - the asset is not well formed. export.validate_fungible_asset # assert that ASSET[1] == ZERO - dup.1 not assert.err=ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO - # => [ASSET] - - # assert that ASSET[2] == ZERO - dup.2 not assert.err=ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_MUST_BE_ZERO + dup.2 not assert.err=ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO # => [ASSET] - # assert that ASSET[3] is a valid account id - dup exec.account::validate_id + # assert that the tuple (ASSET[3], ASSET[2]) forms a valid account id + dup.1 dup.1 exec.account::validate_id # => [ASSET] - # assert that ASSET[3] is a fungible faucet - dup exec.account::is_fungible_faucet - assert.err=ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID + # assert that the first felt (ASSET[3]) of the account id is of type fungible faucet + dup exec.account::is_fungible_faucet + assert.err=ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_AND_THREE_MUST_BE_FUNGIBLE_FAUCET_ID # => [ASSET] # assert that the max amount (ASSET[0]) of a fungible asset is not exceeded @@ -112,18 +101,14 @@ end #! Panics if: #! - the asset is not well formed. export.validate_non_fungible_asset - # assert that ASSET[1] is a valid account id - dup.2 exec.account::validate_id + # assert that ASSET[3] is a valid account id prefix + # hack: because we only have the first felt we add a 0 as the second felt which is always valid + push.0 dup.1 exec.account::validate_id # => [ASSET] - # assert that ASSET[1] is a fungible faucet - dup.2 exec.account::is_non_fungible_faucet - assert.err=ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_FUNGIBLE_FAUCET_ID - # => [ASSET] - - # assert the fungible bit is set to 0 - dup u32split push.FUNGIBLE_BITMASK_U32 u32and - assertz.err=ERR_NON_FUNGIBLE_ASSET_FORMAT_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO drop + # assert that the account ID prefix ASSET[3] is of type non fungible faucet + dup exec.account::is_non_fungible_faucet + assert.err=ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID # => [ASSET] end @@ -170,15 +155,18 @@ end #! Validates that a fungible asset is associated with the provided faucet_id. #! -#! Inputs: [faucet_id, ASSET] +#! Inputs: [faucet_id_hi, faucet_id_lo, ASSET] #! Outputs: [ASSET] #! #! Where: -#! - faucet_id is the account id of the faucet. +#! - faucet_id_hi is the first felt of the faucet's account id. #! - ASSET is the asset to validate. export.validate_fungible_asset_origin # assert the origin of the asset is the faucet_id provided via the stack - dup.1 assert_eq.err=ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN + dup.3 dup.3 + # => [asset_id_hi, asset_id_lo, faucet_id_hi, faucet_id_lo, ASSET] + + exec.account::is_id_eq assert.err=ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN # => [ASSET] # assert the fungible asset is valid @@ -188,15 +176,15 @@ end #! Validates that a non-fungible asset is associated with the provided faucet_id. #! -#! Inputs: [faucet_id, ASSET] +#! Inputs: [faucet_id_hi, ASSET] #! Outputs: [ASSET] #! #! Where: -#! - faucet_id is the account id of the faucet. +#! - faucet_id_hi is the first felt of the faucet's account id. #! - ASSET is the asset to validate. export.validate_non_fungible_asset_origin - # assert the origin of the asset is the faucet_id provided via the stack - dup.3 assert_eq.err=ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN + # assert the origin of the asset is the faucet_id prefix provided via the stack + dup.1 assert_eq.err=ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN # => [ASSET] # assert the non-fungible asset is valid diff --git a/miden-lib/asm/kernels/transaction/lib/asset_vault.masm b/miden-lib/asm/kernels/transaction/lib/asset_vault.masm index e836b2a63..a491d3dad 100644 --- a/miden-lib/asm/kernels/transaction/lib/asset_vault.masm +++ b/miden-lib/asm/kernels/transaction/lib/asset_vault.masm @@ -31,17 +31,23 @@ const.ERR_VAULT_REMOVE_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID=0x0002001E # Failed to remove non-existent non-fungible asset from the vault const.ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND=0x0002001F +# CONSTANTS +# ================================================================================================= + +# The bitmask that when applied will set the fungible bit to zero. +const.INVERSE_FUNGIBLE_BITMASK_U32=0xffffffdf # last byte: 0b1101_1111 + # ACCESSORS # ================================================================================================= #! Returns the balance of a fungible asset associated with a faucet_id. #! -#! Inputs: [faucet_id, vault_root_ptr] +#! Inputs: [faucet_id_hi, faucet_id_lo, vault_root_ptr] #! Outputs: [balance] #! #! Where: #! - vault_root_ptr is a pointer to the memory location at which the vault root is stored. -#! - faucet_id is the faucet id of the fungible asset of interest. +#! - faucet_id_hi is the first felt of the faucet id of the fungible asset of interest. #! - balance is the vault balance of the fungible asset. #! #! Panics if: @@ -50,15 +56,15 @@ export.get_balance # assert that the faucet id is a fungible faucet dup exec.account::is_fungible_faucet assert.err=ERR_VAULT_GET_BALANCE_PROC_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_FAUCET - # => [faucet_id, vault_root_ptr] + # => [faucet_id_hi, faucet_id_lo, vault_root_ptr] # get the asset vault root - padw movup.5 mem_loadw - # => [ASSET_VAULT_ROOT, faucet_id] + padw movup.6 mem_loadw + # => [ASSET_VAULT_ROOT, faucet_id_hi, faucet_id_lo] # prepare the key for fungible asset lookup (pad least significant elements with zeros) - push.0 push.0 push.0 movup.7 - # => [faucet_id, 0, 0, 0, ASSET_VAULT_ROOT] + push.0.0 movup.7 movup.7 + # => [faucet_id_hi, faucet_id_lo, 0, 0, ASSET_VAULT_ROOT] # lookup asset exec.smt::get swapw dropw @@ -83,19 +89,23 @@ end #! - the ASSET is a fungible asset. export.has_non_fungible_asset # check if the asset is a non-fungible asset - exec.asset::is_non_fungible_asset + exec.asset::is_non_fungible_asset assert.err=ERR_VAULT_HAS_NON_FUNGIBLE_ASSET_PROC_CAN_BE_CALLED_ONLY_WITH_NON_FUNGIBLE_ASSET # => [ASSET, vault_root_ptr] + # build the asset key from the non-fungible asset + exec.build_non_fungible_asset_vault_key + # => [ASSET_KEY, vault_root_ptr] + # prepare the stack to read non-fungible asset from vault padw movup.8 mem_loadw swapw - # => [ASSET, ACCT_VAULT_ROOT] + # => [ASSET_KEY, ACCT_VAULT_ROOT] # lookup asset exec.smt::get swapw dropw # => [ASSET] - # compare with EMPTY_WORD to asses if the asset exists in the vault + # compare with EMPTY_WORD to assess if the asset exists in the vault padw eqw not # => [has_asset, PAD, ASSET] @@ -121,41 +131,77 @@ end #! Panics if: #! - the total value of assets is greater than or equal to 2^63. export.add_fungible_asset - push.0 movdn.3 dup movdn.4 - # => [ASSET_KEY, faucet_id, amount, vault_root_ptr] - - # get the asset vault root and read the vault asset value using the `push_smtpeek` decorator. - # To account for the edge case in which CUR_VAULT_VALUE is an EMPTY_WORD, we replace the most - # significant element with the faucet_id to construct the CUR_ASSET. - padw dup.10 mem_loadw swapw + # Create the asset key from the asset. + # --------------------------------------------------------------------------------------------- + + # => [faucet_id_hi, faucet_id_lo, 0, amount, vault_root_ptr] + dupw + # => [faucet_id_hi, faucet_id_lo, 0, amount, faucet_id_hi, faucet_id_lo, 0, amount, vault_root_ptr] + push.0 swap.4 drop + # => [[faucet_id_hi, faucet_id_lo, 0, 0], faucet_id_hi, faucet_id_lo, 0, amount, vault_root_ptr] + movup.6 drop + # => [[faucet_id_hi, faucet_id_lo, 0, 0], faucet_id_hi, faucet_id_lo, amount, vault_root_ptr] + padw dup.11 + # => [vault_root_ptr, pad(4), ASSET_KEY, faucet_id_hi, faucet_id_lo, amount, vault_root_ptr] + + # Get the asset vault root and read the current asset using the `push_smtpeek` decorator. + # --------------------------------------------------------------------------------------------- + + # the current asset may be the empty word if it does not exist and so its faucet id would be zeroes + # we therefore overwrite the faucet id with the faucet id from ASSET to account for this edge case + mem_loadw swapw + # => [ASSET_KEY, VAULT_ROOT, faucet_id_hi, faucet_id_lo, amount, vault_root_ptr] adv.push_smtpeek push.15329 drop # TODO: remove line, see miden-vm/#1122 - adv_loadw swapw dupw.1 drop movup.11 - # => [CUR_ASSET, VAULT_ROOT, CUR_VAULT_VALUE, amount, vault_root_ptr] - - # arrange elements + adv_loadw + # => [CUR_VAULT_VALUE, VAULT_ROOT, faucet_id_hi, faucet_id_lo, amount, vault_root_ptr] + swapw + # => [VAULT_ROOT, CUR_VAULT_VALUE, faucet_id_hi, faucet_id_lo, amount, vault_root_ptr] + dupw.1 + # => [CUR_VAULT_VALUE, VAULT_ROOT, CUR_VAULT_VALUE, faucet_id_hi, faucet_id_lo, amount, vault_root_ptr] + drop drop + # => [[0, cur_amount], VAULT_ROOT, CUR_VAULT_VALUE, faucet_id_hi, faucet_id_lo, amount, vault_root_ptr] + movup.11 movup.11 + # => [[faucet_id_hi, faucet_id_lo, 0, cur_amount], VAULT_ROOT, CUR_VAULT_VALUE, amount, vault_root_ptr] + + # Check the new amount does not exceed the maximum allowed amount and add the two + # fungible assets together. + # --------------------------------------------------------------------------------------------- + + # arrange amounts movup.3 movup.12 dup - # => [amount, amount, cur_amount, faucet_id, 0, 0, VAULT_ROOT, CUR_VAULT_VALUE, vault_root_ptr] + # => [amount, amount, cur_amount, faucet_id_hi, faucet_id_lo, 0, VAULT_ROOT, CUR_VAULT_VALUE, vault_root_ptr] # compute max_amount - cur_amount exec.asset::get_fungible_asset_max_amount dup.3 sub - # => [(max_amount - cur_amount), amount, amount, cur_amount, faucet_id, 0, 0, VAULT_ROOT, + # => [(max_amount - cur_amount), amount, amount, cur_amount, faucet_id_hi, faucet_id_lo, 0, VAULT_ROOT, # CUR_VAULT_VALUE, vault_root_ptr] # assert amount + cur_amount < max_amount lte assert.err=ERR_VAULT_FUNGIBLE_MAX_AMOUNT_EXCEEDED - # => [amount, cur_amount, faucet_id, 0, 0, VAULT_ROOT, CUR_VAULT_VALUE, vault_root_ptr] + # => [amount, cur_amount, faucet_id_hi, faucet_id_lo, 0, VAULT_ROOT, CUR_VAULT_VALUE, vault_root_ptr] # add asset amounts add movdn.3 # => [ASSET', VAULT_ROOT, CUR_VAULT_VALUE, vault_root_ptr] - # prepare the stack to insert the asset into the vault - dupw movdnw.3 dupw movup.3 drop push.0 movdn.3 swapw - # => [ASSET', KEY, VAULT_ROOT, CUR_VAULT_VALUE, ASSET', vault_root_ptr] + # Create the asset key and insert the updated asset. + # --------------------------------------------------------------------------------------------- + + # create the asset key to prepare insertion of the asset into the vault + dupw movdnw.3 + # => [ASSET', VAULT_ROOT, CUR_VAULT_VALUE, ASSET', vault_root_ptr] + dupw + # => [ASSET', ASSET', VAULT_ROOT, CUR_VAULT_VALUE, ASSET', vault_root_ptr] + push.0 swap.4 drop + # => [[faucet_id_hi, faucet_id_lo, 0, 0], ASSET', VAULT_ROOT, CUR_VAULT_VALUE, ASSET', vault_root_ptr] + swapw + # => [ASSET', ASSET_KEY', VAULT_ROOT, CUR_VAULT_VALUE, ASSET', vault_root_ptr] # update asset in vault and assert the old value is equivalent to the value provided via the # decorator - exec.smt::set movupw.2 assert_eqw.err=ERR_VAULT_ADD_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID + exec.smt::set + # => [PREV_ASSET, VAULT_ROOT', CUR_VAULT_VALUE, ASSET', vault_root_ptr] + movupw.2 assert_eqw.err=ERR_VAULT_ADD_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID # => [VAULT_ROOT', ASSET', vault_root_ptr] # update the vault root @@ -175,15 +221,27 @@ end #! Panics if: #! - the vault already contains the same non-fungible asset. export.add_non_fungible_asset - # prepare the stack to insert the asset into the vault - dup.4 movdn.5 dupw padw movup.12 mem_loadw swapw dupw - # => [ASSET, ASSET, VAULT_ROOT, ASSET, vault_root_ptr] + # Build the asset key from the non-fungible asset. + # --------------------------------------------------------------------------------------------- + + dupw exec.build_non_fungible_asset_vault_key + # => [ASSET_KEY, ASSET, vault_root_ptr] + + # Load VAULT_ROOT and insert asset. + # --------------------------------------------------------------------------------------------- + + padw dup.12 + # => [vault_root_ptr, pad(4), ASSET_KEY, ASSET, vault_root_ptr] + mem_loadw swapw + # => [ASSET_KEY, VAULT_ROOT, ASSET, vault_root_ptr] + dupw.2 + # => [ASSET, ASSET_KEY, VAULT_ROOT, ASSET, vault_root_ptr] # insert asset into vault exec.smt::set # => [OLD_VAL, VAULT_ROOT', ASSET, vault_root_ptr] - # Assert old value was empty + # assert old value was empty padw assert_eqw.err=ERR_VAULT_NON_FUNGIBLE_ASSET_ALREADY_EXISTS # => [VAULT_ROOT', ASSET, vault_root_ptr] @@ -308,17 +366,24 @@ end #! Panics if: #! - the non-fungible asset is not found in the vault. export.remove_non_fungible_asset - # prepare the stack to insert an EMPTY_WORD into the vault at key associated with the - # non-fungible asset - dup.4 movdn.5 dupw padw movup.12 mem_loadw swapw padw - # => [EMPTY_WORD, ASSET, VAULT_ROOT, ASSET, vault_root_ptr] + # build non-fungible asset key + dupw exec.build_non_fungible_asset_vault_key padw + # => [pad(4), ASSET_KEY, ASSET, vault_root_ptr] + + # load vault root + dup.12 mem_loadw + # => [VAULT_ROOT, ASSET_KEY, ASSET, vault_root_ptr] + + # prepare insertion of an EMPTY_WORD into the vault at the asset key to remove the asset + swapw padw + # => [EMPTY_WORD, ASSET_KEY, VAULT_ROOT, ASSET, vault_root_ptr] # update asset in vault exec.smt::set # => [OLD_VAL, VAULT_ROOT', ASSET, vault_root_ptr] - # Assert old value was not empty (we only need to check ASSET[1] which is the faucet id) - drop drop eq.0 assertz.err=ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND drop + # assert old value was not empty (we only need to check ASSET[3] which is the faucet id) + eq.0 assertz.err=ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND drop drop drop # => [VAULT_ROOT', ASSET, vault_root_ptr] # update the vault root @@ -353,3 +418,37 @@ export.remove_asset # => [ASSET] end end + +# HELPER PROCEDURES +# ================================================================================================= + +#! Builds the vault key of a non fungible asset. The asset is NOT validated and therefore must +#! be a valid non-fungible asset. +#! +#! Inputs: [ASSET] +#! Outputs: [ASSET_KEY] +#! +#! Where: +#! - ASSET is the non-fungible asset for which the vault key is built. +#! - ASSET_KEY is the vault key of the non-fungible asset. +proc.build_non_fungible_asset_vault_key + # Create the asset key from the non-fungible asset. + # --------------------------------------------------------------------------------------------- + + # swap hash0 with faucet id + # => [faucet_id_hi, hash2, hash1, hash0] + swap.3 + # => [hash0, hash2, hash1 faucet_id_hi] + + # disassemble hash0 into u32 limbs + u32split swap + # => [hash0_lo, hash0_hi, hash2, hash1 faucet_id_hi] + + # set the fungible bit to 0 + u32and.INVERSE_FUNGIBLE_BITMASK_U32 + # => [hash0_lo', hash0_hi, hash2, hash1 faucet_id_hi] + + # reassemble hash0 felt by multiplying the high part with 2^32 and adding the lo part + swap push.0x0100000000 mul add + # => [ASSET_KEY] +end \ No newline at end of file diff --git a/miden-lib/asm/kernels/transaction/lib/faucet.masm b/miden-lib/asm/kernels/transaction/lib/faucet.masm index 364957763..f5fcb6817 100644 --- a/miden-lib/asm/kernels/transaction/lib/faucet.masm +++ b/miden-lib/asm/kernels/transaction/lib/faucet.masm @@ -145,7 +145,8 @@ end proc.mint_non_fungible_asset # assert that the asset is associated with the faucet the transaction is being executed against # and that the asset is valid - exec.account::get_id exec.asset::validate_non_fungible_asset_origin + exec.account::get_id swap drop + exec.asset::validate_non_fungible_asset_origin # => [ASSET] # fetch the root of the SMT containing the non-fungible assets diff --git a/miden-lib/asm/kernels/transaction/lib/memory.masm b/miden-lib/asm/kernels/transaction/lib/memory.masm index 513d04953..87e5450df 100644 --- a/miden-lib/asm/kernels/transaction/lib/memory.masm +++ b/miden-lib/asm/kernels/transaction/lib/memory.masm @@ -49,7 +49,7 @@ const.GLOBAL_INPUTS_SECTION_OFFSET=100 # The memory address at which the latest known block hash is stored const.BLK_HASH_PTR=100 -# The memory address at which the account id is stored +# The memory address at which the account id felts are stored. const.ACCT_ID_PTR=101 # The memory address at which the initial account hash is stored @@ -330,24 +330,30 @@ end #! Sets the account id. #! -#! Inputs: [acct_id] +#! Inputs: [acct_id_hi, acct_id_lo] #! Outputs: [] #! #! Where: -#! - acct_id is the account id. +#! - acct_id_{hi,lo} are the first and second felt of the account id. export.set_global_acct_id - push.ACCT_ID_PTR mem_store + push.0.0 + # => [0, 0, acct_id_hi, acct_id_lo] + mem_storew.ACCT_ID_PTR + dropw + # => [] end #! Returns the global account id. #! #! Inputs: [] -#! Outputs: [acct_id] +#! Outputs: [acct_id_hi, acct_id_lo] #! #! Where: -#! - acct_id is the account id. +#! - acct_id_{hi,lo} are the first and second felt of the account id. export.get_global_acct_id - push.ACCT_ID_PTR mem_load + padw mem_loadw.ACCT_ID_PTR + # => [0, 0, acct_id_hi, acct_id_lo] + drop drop end #! Sets the initial account hash. @@ -725,22 +731,25 @@ end #! Returns the id of the current account. #! #! Inputs: [] -#! Outputs: [curr_acct_id] +#! Outputs: [curr_acct_id_hi, curr_acct_id_lo] #! #! Where: -#! - curr_acct_id is the account id of the currently accessing account. +#! - curr_acct_id_{hi,lo} are the first and second felt of the account id of the currently +#! accessing account. export.get_account_id - exec.get_current_account_data_ptr push.ACCT_ID_AND_NONCE_OFFSET add - mem_load + padw exec.get_current_account_data_ptr push.ACCT_ID_AND_NONCE_OFFSET add mem_loadw + # => [nonce, 0, curr_acct_id_hi, curr_acct_id_lo] + drop drop + # => [curr_acct_id_hi, curr_acct_id_lo] end #! Sets the account id and nonce. #! -#! Inputs: [account_nonce, 0, 0, account_id] -#! Outputs: [account_nonce, 0, 0, account_id] +#! Inputs: [account_nonce, 0, account_id_hi, account_id_lo] +#! Outputs: [account_nonce, 0, account_id_hi, account_id_lo] #! #! Where: -#! - account_id is the id of the currently accessing account. +#! - account_id_{hi,lo} are the first and second felt of the id of the currently accessing account. #! - account_nonce is the nonce of the currently accessing account. export.set_acct_id_and_nonce exec.get_current_account_data_ptr push.ACCT_ID_AND_NONCE_OFFSET add @@ -750,13 +759,15 @@ end #! Returns the id of the native account. #! #! Inputs: [] -#! Outputs: [native_acct_id] +#! Outputs: [native_acct_id_hi, native_acct_id_lo] #! #! Where: -#! - native_acct_id is the id of the native account. +#! - native_acct_id_{hi,lo} are the first and second felt of the id of the native account. export.get_native_account_id - push.NATIVE_ACCOUNT_DATA_PTR push.ACCT_ID_AND_NONCE_OFFSET add - mem_load + padw push.NATIVE_ACCOUNT_DATA_PTR push.ACCT_ID_AND_NONCE_OFFSET add mem_loadw + # => [nonce, 0, native_acct_id_hi, native_acct_id_lo] + drop drop + # => [native_acct_id_hi, native_acct_id_lo] end #! Returns the account nonce. @@ -781,12 +792,12 @@ end #! Where: #! - acct_nonce is the account nonce. export.set_acct_nonce - exec.get_current_account_data_ptr push.ACCT_ID_AND_NONCE_OFFSET add - padw dup.4 - mem_loadw - # => [old_nonce, 0, 0, old_id, acct_id_and_nonce_ptr, new_nonce] - + exec.get_current_account_data_ptr push.ACCT_ID_AND_NONCE_OFFSET add padw + # => [0, 0, 0, 0, acct_id_and_nonce_ptr, new_nonce] + dup.4 mem_loadw + # => [old_nonce, 0, old_id_hi, old_id_lo, acct_id_and_nonce_ptr, new_nonce] drop movup.4 movup.4 mem_storew dropw + # => [] end ### ACCOUNT VAULT ################################################# @@ -1250,7 +1261,7 @@ end #! Returns the sender for the input note located at the specified memory address. #! #! Inputs: [note_ptr] -#! Outputs: [sender] +#! Outputs: [sender_hi, sender_lo] #! #! Where: #! - note_ptr is the memory address at which the input note data begins. @@ -1259,10 +1270,23 @@ export.get_input_note_sender padw movup.4 push.INPUT_NOTE_METADATA_OFFSET add mem_loadw - # => [aux, encoded_type_and_ex_hint, sender, tag] + # => [aux, merged_tag_hint_payload, merged_sender_id_type_hint_tag, sender_hi] + + drop drop + # => [merged_sender_id_type_hint_tag, sender_hi] + + # extract second felt of sender from merged layout, which means clearing the least significant byte + u32split swap + # => [merged_lo, merged_hi, sender_hi] + + # clear least significant byte + u32and.0xffffff00 swap + # => [sender_lo_hi, sender_lo_lo, sender_hi] + + # reassemble the second felt by multiplying the high part with 2^32 and adding the lo part + mul.0x0100000000 add swap + # => [sender_hi, sender_lo] - drop drop swap drop - # => [sender] end # OUTPUT NOTES diff --git a/miden-lib/asm/kernels/transaction/lib/note.masm b/miden-lib/asm/kernels/transaction/lib/note.masm index 2b7dbea3b..c69b6b2df 100644 --- a/miden-lib/asm/kernels/transaction/lib/note.masm +++ b/miden-lib/asm/kernels/transaction/lib/note.masm @@ -31,10 +31,10 @@ const.OUTPUT_NOTE_HASHING_MEM_DIFF=510 #! Returns the sender of the note currently being processed. #! #! Inputs: [] -#! Outputs: [sender] +#! Outputs: [sender_hi, sender_lo] #! #! Where: -#! - sender is the sender of the note currently being processed. +#! - sender_{hi,lo} are the first and second felt of the sender of the note currently being processed. #! #! Panics if: #! - the note is not being processed. @@ -50,7 +50,7 @@ export.get_sender # get the sender from the note pointer exec.memory::get_input_note_sender - # => [sender] + # => [sender_hi, sender_lo] end #! Returns the number of assets and the assets hash of the note currently being processed. diff --git a/miden-lib/asm/kernels/transaction/lib/prologue.masm b/miden-lib/asm/kernels/transaction/lib/prologue.masm index d3305a4bf..f72943eef 100644 --- a/miden-lib/asm/kernels/transaction/lib/prologue.masm +++ b/miden-lib/asm/kernels/transaction/lib/prologue.masm @@ -58,17 +58,21 @@ const.ERR_PROLOGUE_NUMBER_OF_INPUT_NOTES_EXCEEDS_LIMIT=0x00020040 # Note commitment computed from the input note data does not match given note commitment const.ERR_PROLOGUE_INPUT_NOTES_COMMITMENT_MISMATCH=0x00020041 +# New account must have a zero nonce +const.ERR_PROLOGUE_NEW_ACCOUNT_NONCE_MUST_BE_ZERO=0x0002005B + # PUBLIC INPUTS # ================================================================================================= #! Saves global inputs to memory. #! -#! Inputs: [BLOCK_HASH, acct_id, INITIAL_ACCOUNT_HASH, INPUT_NOTES_COMMITMENT] +#! Inputs: [BLOCK_HASH, account_id_hi, account_id_lo, INITIAL_ACCOUNT_HASH, INPUT_NOTES_COMMITMENT] #! Outputs: [] #! #! Where: #! - BLOCK_HASH is the reference block for the transaction execution. -#! - acct_id is the account id of the account that the transaction is being executed against. +#! - account_id_{hi,lo} are the first and second felt of the account id of the account that the +#! transaction is being executed against. #! - INITIAL_ACCOUNT_HASH is the account state prior to the transaction, EMPTY_WORD for new #! accounts. #! - INPUT_NOTES_COMMITMENT is the see `transaction::api::get_input_notes_commitment`. @@ -317,7 +321,7 @@ proc.validate_new_account # => [] # Assert the account nonce is 0 - exec.memory::get_acct_nonce eq.0 assert + exec.memory::get_acct_nonce eq.0 assert.err=ERR_PROLOGUE_NEW_ACCOUNT_NONCE_MUST_BE_ZERO # => [] # Assert the initial vault is empty @@ -336,14 +340,14 @@ proc.validate_new_account # Assert faucet reserved slot is correctly initialized # --------------------------------------------------------------------------------------------- # check if the account is a faucet - exec.account::get_id dup exec.account::is_faucet - # => [is_faucet, acct_id] + exec.account::get_id swap drop dup exec.account::is_faucet + # => [is_faucet, acct_id_hi] # process conditional logic depending on whether the account is a faucet if.true # get the faucet reserved slot exec.account::get_faucet_storage_data_slot exec.account::get_item - # => [FAUCET_RESERVED_SLOT, acct_id] + # => [FAUCET_RESERVED_SLOT, acct_id_hi] # check if the account is a fungible faucet movup.4 exec.account::is_fungible_faucet @@ -382,7 +386,7 @@ proc.validate_new_account # => [] end else - # drop the account id + # drop the hi part of the ID drop # => [] end @@ -425,9 +429,9 @@ end #! Inputs: #! Operand stack: [] #! Advice stack: [ -#! account_id, 0, 0, account_nonce, -#! ACCOUNT_VAULT_ROOT, -#! ACCOUNT_STORAGE_COMMITMENT, +#! account_id_lo, account_id_hi, 0, account_nonce, +#! ACCOUNT_VAULT_ROOT, +#! ACCOUNT_STORAGE_COMMITMENT, #! ACCOUNT_CODE_COMMITMENT #! ] #! Outputs: @@ -435,7 +439,8 @@ end #! Advice stack: [] #! #! Where: -#! - account_id is the account that the transaction is being executed against. +#! - account_id_{hi,lo} are the first and second felt of the ID of the account that the transaction +#! is being executed against. #! - account_nonce is the account's nonce. #! - ACCOUNT_VAULT_ROOT is the account's vault root. #! - ACCOUNT_STORAGE_COMMITMENT is the account's storage commitment. @@ -464,7 +469,7 @@ proc.process_account_data # assert the account id matches the account id in global inputs exec.memory::get_global_acct_id exec.memory::get_account_id - assert_eq.err=ERR_PROLOGUE_MISMATCH_OF_ACCOUNT_IDS_FROM_GLOBAL_INPUTS_AND_ADVICE_PROVIDER + exec.account::is_id_eq assert.err=ERR_PROLOGUE_MISMATCH_OF_ACCOUNT_IDS_FROM_GLOBAL_INPUTS_AND_ADVICE_PROVIDER # => [ACCT_HASH] # store a copy of the initial nonce in global inputs @@ -666,7 +671,7 @@ proc.process_note_args_and_metadata # => [NOTE_METADATA] end -#! Copies the note's assets the advice stack to memory and verifies the commitment. +#! Copies the note's assets from the advice stack to memory and verifies the commitment. #! #! Inputs: #! Operand stack: [note_ptr] @@ -1081,7 +1086,7 @@ end #! Inputs: #! Operand stack: [ #! BLOCK_HASH, -#! account_id, +#! account_id_hi, account_id_lo, #! INITIAL_ACCOUNT_HASH, #! INPUT_NOTES_COMMITMENT, #! ] @@ -1096,7 +1101,7 @@ end #! [block_num, version, timestamp, 0], #! NOTE_ROOT, #! kernel_version -#! [account_id, 0, 0, account_nonce], +#! [account_id_lo, account_id_hi, 0, account_nonce], #! ACCOUNT_VAULT_ROOT, #! ACCOUNT_STORAGE_COMMITMENT, #! ACCOUNT_CODE_COMMITMENT, @@ -1117,7 +1122,8 @@ end #! #! Where: #! - BLOCK_HASH is the reference block for the transaction execution. -#! - account_id is the account that the transaction is being executed against. +#! - account_id_{hi,lo} are the first and second felt of the account that the transaction is being +#! executed against. #! - INITIAL_ACCOUNT_HASH is the account state prior to the transaction, EMPTY_WORD for new #! accounts. #! - INPUT_NOTES_COMMITMENT, see `transaction::api::get_input_notes_commitment`. diff --git a/miden-lib/asm/kernels/transaction/lib/tx.masm b/miden-lib/asm/kernels/transaction/lib/tx.masm index 71fe80173..0e0399463 100644 --- a/miden-lib/asm/kernels/transaction/lib/tx.masm +++ b/miden-lib/asm/kernels/transaction/lib/tx.masm @@ -366,7 +366,9 @@ end #! - execution_hint is the hint which specifies when a note is ready to be consumed. #! - NOTE_METADATA is the metadata associated with a note. proc.build_note_metadata - # validate the note type + # Validate the note type. + # -------------------------------------------------------------------------------------------- + # NOTE: encrypted notes are currently unsupported dup.2 push.PRIVATE_NOTE eq dup.3 push.PUBLIC_NOTE eq or assert.err=ERR_NOTE_INVALID_TYPE # => [tag, aux, note_type, execution_hint] @@ -383,16 +385,73 @@ proc.build_note_metadata assert_eq.err=ERR_NOTE_INVALID_NOTE_TYPE_FOR_NOTE_TAG_PREFIX # => [tag, aux, note_type, execution_hint] - # encode note_type and execution_hint into a single element - movup.3 movup.3 push.TWO_POW_38 mul add movup.2 - # => [aux, encoded_type_and_ex_hint, tag] + # Split execution hint into its tag and payload parts as they are encoded in separate elements + # of the metadata. + # -------------------------------------------------------------------------------------------- + + # the execution_hint is layed out like this: [26 zero bits | payload (32 bits) | tag (6 bits)] + movup.3 + # => [execution_hint, tag, aux, note_type] + dup u32split drop + # => [execution_hint_lo, execution_hint, tag, aux, note_type] + + # mask out the lower 6 execution hint tag bits. + push.0x3f u32and + # => [execution_hint_tag, execution_hint, tag, aux, note_type] + + # compute the payload by subtracting the tag value so the lower 6 bits are zero + # note that this results in the following layout: [26 zero bits | payload (32 bits) | 6 zero bits] + swap + # => [execution_hint, execution_hint_tag, tag, aux, note_type] + dup.1 + # => [execution_hint_tag, execution_hint, execution_hint_tag, tag, aux, note_type] + sub + # => [execution_hint_payload, execution_hint_tag, tag, aux, note_type] + + # Merge execution hint payload and note tag. + # -------------------------------------------------------------------------------------------- + + # we need to move the payload to the upper 32 bits of the felt + # we only need to shift by 26 bits because the payload is already shifted left by 6 bits + # we shift the payload by multiplying with 2^26 + # this results in the lower 32 bits being zero which is where the note tag will be added + push.0x04000000 mul + # => [execution_hint_payload, execution_hint_tag, tag, aux, note_type] + + # add the tag to the payload to produce the merged value + movup.2 add + # => [note_tag_hint_payload, execution_hint_tag, aux, note_type] + + # Merge sender_lo, note_type and execution_hint_tag. + # -------------------------------------------------------------------------------------------- - # add sender account ID to metadata exec.account::get_id - # => [sender_acct_id, aux, encoded_type_and_ex_hint, tag] - - movdn.2 - # => [NOTE_METADATA] + # => [sender_hi, sender_lo, note_tag_hint_payload, execution_hint_tag, aux, note_type] + + movup.5 + # => [note_type, sender_hi, sender_lo, note_tag_hint_payload, execution_hint_tag, aux] + # multiply by 2^6 to shift the two note_type bits left by 6 bits. + push.0x40 mul + # => [shifted_note_type, sender_hi, sender_lo, note_tag_hint_payload, execution_hint_tag, aux] + + # merge execution_hint_tag into the note_type + # this produces an 8-bit value with the layout: [note_type (2 bits) | execution_hint_tag (6 bits)] + movup.4 add + # => [merged_note_type_execution_hint_tag, sender_hi, sender_lo, note_tag_hint_payload, aux] + + # merge sender_lo into this value + movup.2 add + # => [sender_lo_type_and_hint_tag, sender_hi, note_tag_hint_payload, aux] + + # Rearrange elements to produce the final note metadata layout. + # -------------------------------------------------------------------------------------------- + + swap movdn.3 + # => [sender_lo_type_and_hint_tag, note_tag_hint_payload, aux, sender_hi] + swap + # => [note_tag_hint_payload, sender_lo_type_and_hint_tag, aux, sender_hi] + movup.2 + # => [NOTE_METADATA = [aux, note_tag_hint_payload, sender_lo_type_and_hint_tag, sender_hi]] end #! Creates a new note and returns the index of the note. diff --git a/miden-lib/asm/miden/account.masm b/miden-lib/asm/miden/account.masm index b87406d89..7c10c064b 100644 --- a/miden-lib/asm/miden/account.masm +++ b/miden-lib/asm/miden/account.masm @@ -6,10 +6,10 @@ use.miden::kernel_proc_offsets #! Returns the account id. #! #! Inputs: [] -#! Outputs: [acct_id] +#! Outputs: [acct_id_hi, acct_id_lo] #! #! Where: -#! - acct_id is the account id. +#! - acct_id_{hi,lo} are the first and second felt of the account id. #! #! Invocation: exec export.get_id @@ -24,11 +24,11 @@ export.get_id # => [offset, pad(15)] syscall.exec_kernel_proc - # => [acct_id, pad(15)] + # => [acct_id_hi, acct_id_lo, pad(14)] # clean the stack - swapdw dropw dropw swapw dropw movdn.3 drop drop drop - # => [acct_id] + swapdw dropw dropw swapw dropw movdn.3 movdn.3 drop drop + # => [acct_id_hi, acct_id_lo] end #! Returns the account nonce. @@ -191,7 +191,7 @@ export.set_item # pad the stack push.0.0 movdn.7 movdn.7 padw padw swapdw # => [offset, index, V', pad(10)] - + syscall.exec_kernel_proc # => [R', V, pad(8)] @@ -294,11 +294,12 @@ end #! Returns the balance of a fungible asset associated with a faucet_id. #! -#! Inputs: [faucet_id] +#! Inputs: [faucet_id_hi, faucet_id_lo] #! Outputs: [balance] #! #! Where: -#! - faucet_id is the faucet id of the fungible asset of interest. +#! - faucet_id_{hi,lo} are the first and second felt of the faucet id of the fungible asset +#! of interest. #! - balance is the vault balance of the fungible asset. #! #! Panics if: @@ -307,11 +308,11 @@ end #! Invocation: exec export.get_balance exec.kernel_proc_offsets::account_vault_get_balance_offset - # => [offset, faucet_id] + # => [offset, faucet_id_hi, faucet_id_lo] # pad the stack - push.0.0 movdn.3 movdn.3 padw swapw padw padw swapdw - # => [offset, faucet_id, pad(14)] + push.0 movdn.3 padw swapw padw padw swapdw + # => [offset, faucet_id_hi, faucet_id_lo, pad(13)] syscall.exec_kernel_proc # => [balance, pad(15)] @@ -441,16 +442,16 @@ end # PROCEDURES COPIED FROM KERNEL (TODO: get rid of this duplication) # ================================================================================================= -# Given the most significant half of an account id, this mask defines the bits used to determine the -# account type. -const.ACCOUNT_TYPE_U32MASK=805306368 # 0b00110000_00000000_00000000_00000000 +# Given the least significant 32 bits of an account id's first felt, this mask defines the bits used +# to determine the account type. +const.ACCOUNT_ID_TYPE_MASK_U32=0x30 # 0b11_0000 # Bit pattern for a fungible faucet w/ immutable code, after the account type mask has been applied. -const.FUNGIBLE_FAUCET_ACCOUNT=536870912 # 0b00100000_00000000_00000000_00000000 +const.FUNGIBLE_FAUCET_ACCOUNT=0x20 # 0b10_0000 -# Bit pattern for a non-fungible faucet w/ immutable code, after the account type mask has been +# Bit pattern for a non-fungible faucet w/ immutable code, after the account type mask has been # applied. -const.NON_FUNGIBLE_FAUCET_ACCOUNT=805306368 # 0b00110000_00000000_00000000_00000000 +const.NON_FUNGIBLE_FAUCET_ACCOUNT=0x30 # 0b11_0000 #! Returns the most significant half with the account type bits masked out. #! @@ -461,13 +462,13 @@ const.NON_FUNGIBLE_FAUCET_ACCOUNT=805306368 # 0b00110000_00000000_00000000_00000 #! - FUNGIBLE_FAUCET_ACCOUNT #! - NON_FUNGIBLE_FAUCET_ACCOUNT #! -#! Stack: [acct_id] +#! Stack: [acct_id_hi] #! Output: [acct_type] #! -#! - acct_id is the account id. +#! - acct_id_hi is the first felt of the account id. #! - acct_type is the account type. proc.type - u32split swap drop push.ACCOUNT_TYPE_U32MASK u32and + u32split drop push.ACCOUNT_ID_TYPE_MASK_U32 u32and # => [acct_type] end @@ -494,3 +495,23 @@ export.is_non_fungible_faucet exec.type push.NON_FUNGIBLE_FAUCET_ACCOUNT eq # => [is_non_fungible_faucet] end + +#! TODO: This is a copy; move to utils. +#! +#! Returns a boolean indicating whether the given account_ids are equal. +#! +#! Inputs: [acct_id_hi, acct_id_lo, other_acct_id_hi, other_acct_id_lo] +#! Outputs: [is_id_equal] +#! +#! Where: +#! - acct_id_{hi,lo} are the first and second felt of an account id. +#! - other_acct_id_{hi,lo} are the first and second felt of the other account id to compare against. +#! - is_id_equal is a boolean indicating whether the account ids are equal. +export.is_id_eq + movup.2 eq + # => [is_hi_equal, acct_id_lo, other_acct_id_lo] + swap movup.2 eq + # => [is_lo_equal, is_hi_equal] + and + # => [is_id_equal] +end diff --git a/miden-lib/asm/miden/asset.masm b/miden-lib/asm/miden/asset.masm index 1ba20aade..b421f1fec 100644 --- a/miden-lib/asm/miden/asset.masm +++ b/miden-lib/asm/miden/asset.masm @@ -12,25 +12,16 @@ const.ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT=0x0002004C # Failed to build the non-fungible asset because the provided faucet id is not from a non-fungible faucet const.ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID=0x0002004D -# CONSTANTS -# ================================================================================================= - -# Two raised to the power of 32 (2^32) -const.TWO_POW_32=4294967296 - -# The bit 29 of the most significant half of the element is used to identify the asset type -const.FUNGIBLE_BITMASK_U32=0x20000000 - # PROCEDURES # ================================================================================================= #! Builds a fungible asset for the specified fungible faucet and amount. #! -#! Inputs: [faucet_id, amount] +#! Inputs: [faucet_id_hi, faucet_id_lo, amount] #! Outputs: [ASSET] #! #! Where: -#! - faucet_id is the faucet to create the asset for. +#! - faucet_id_{hi,lo} are the first and second felt of the faucet to create the asset for. #! - amount is the amount of the asset to create. #! - ASSET is the built fungible asset. #! @@ -38,15 +29,15 @@ const.FUNGIBLE_BITMASK_U32=0x20000000 export.build_fungible_asset # assert the faucet is a fungible faucet dup exec.account::is_fungible_faucet assert.err=ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID - # => [faucet_id, amount] + # => [faucet_id_hi, faucet_id_lo, amount] # assert the amount is valid - dup.1 exec.get_fungible_asset_max_amount lte + dup.2 exec.get_fungible_asset_max_amount lte assert.err=ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT - # => [faucet_id, amount] + # => [faucet_id_hi, faucet_id_lo, amount] # create the asset - push.0.0 movup.2 + push.0 movdn.2 # => [ASSET] end @@ -63,7 +54,7 @@ end export.create_fungible_asset # fetch the id of the faucet the transaction is being executed against. exec.account::get_id - # => [id, amount] + # => [id_hi, id_lo, amount] # build the fungible asset exec.build_fungible_asset @@ -72,11 +63,11 @@ end #! Builds a non fungible asset for the specified non-fungible faucet and amount. #! -#! Inputs: [faucet_id, DATA_HASH] +#! Inputs: [faucet_id_hi, DATA_HASH] #! Outputs: [ASSET] #! #! Where: -#! - faucet_id is the faucet to create the asset for. +#! - faucet_id_{hi,lo} are the first and second felt of the faucet to create the asset for. #! - DATA_HASH is the data hash of the non-fungible asset to build. #! - ASSET is the built non-fungible asset. #! @@ -85,14 +76,11 @@ export.build_non_fungible_asset # assert the faucet is a non-fungible faucet dup exec.account::is_non_fungible_faucet assert.err=ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID - # => [faucet_id, DATA_HASH] + # => [faucet_id_hi, hash3, hash2, hash1, hash0] # build the asset - movup.3 drop movdn.2 - # => [hash_0, hash_1, faucet_id, hash_3] - - # Force the non-fungible bit to 0 - u32split dup push.FUNGIBLE_BITMASK_U32 u32and u32xor push.TWO_POW_32 mul add + swap drop + # => [faucet_id_hi, hash2, hash1, hash0] # => [ASSET] end @@ -108,8 +96,8 @@ end #! Invocation: exec export.create_non_fungible_asset # get the id of the faucet the transaction is being executed against - exec.account::get_id - # => [id, DATA_HASH] + exec.account::get_id swap drop + # => [faucet_id_hi, DATA_HASH] # build the non-fungible asset exec.build_non_fungible_asset diff --git a/miden-lib/asm/miden/contracts/auth/basic.masm b/miden-lib/asm/miden/contracts/auth/basic.masm index 757446550..36c205a7d 100644 --- a/miden-lib/asm/miden/contracts/auth/basic.masm +++ b/miden-lib/asm/miden/contracts/auth/basic.masm @@ -25,11 +25,11 @@ export.auth_tx_rpo_falcon512 # => [0, 0, 0, nonce, INPUT_NOTES_HASH, OUTPUT_NOTES_HASH, pad(16)] # Get current AccountID and pad - exec.account::get_id push.0.0.0 - # => [0, 0, 0, account_id, 0, 0, 0, nonce, INPUT_NOTES_HASH, OUTPUT_NOTES_HASH, pad(16)] + exec.account::get_id push.0.0 + # => [0, 0, account_id_hi, account_id_lo, 0, 0, 0, nonce, INPUT_NOTES_HASH, OUTPUT_NOTES_HASH, pad(16)] # Compute the message to be signed - # MESSAGE = h(OUTPUT_NOTES_HASH, h(INPUT_NOTES_HASH, h(0, 0, 0, account_id, 0, 0, 0, nonce))) + # MESSAGE = h(OUTPUT_NOTES_HASH, h(INPUT_NOTES_HASH, h(0, 0, account_id_hi, account_id_lo, 0, 0, 0, nonce))) hmerge hmerge hmerge # => [MESSAGE, pad(16)] diff --git a/miden-lib/asm/miden/note.masm b/miden-lib/asm/miden/note.masm index 10ff51a13..41f304c0a 100644 --- a/miden-lib/asm/miden/note.masm +++ b/miden-lib/asm/miden/note.masm @@ -144,10 +144,10 @@ end #! Returns the sender of the note currently being processed. #! #! Inputs: [] -#! Outputs: [sender] +#! Outputs: [sender_hi, sender_lo] #! #! Where: -#! - sender is the sender of the note currently being processed. +#! - sender_{hi,lo} are the first and second felt of the sender of the note currently being processed. #! #! Panics if: #! - no note is being processed. @@ -165,8 +165,8 @@ export.get_sender # => [sender, pad(15)] # clean the stack - swapdw dropw dropw swapw dropw movdn.3 drop drop drop - # => [sender] + swapdw dropw dropw swapw dropw movdn.3 movdn.3 drop drop + # => [sender_hi, sender_lo] end #! Returns the serial number of the note currently being processed. @@ -218,7 +218,7 @@ end #! Invocation: exec export.compute_inputs_hash # check that number of inputs is less than 128 - dup.1 push.128 u32assert2 u32lte assert + dup.1 push.128 u32assert2 u32lte assert.err=ERR_PROLOGUE_NUMBER_OF_NOTE_INPUTS_EXCEEDED_LIMIT # compute the hash exec.rpo::hash_memory diff --git a/miden-lib/asm/miden/tx.masm b/miden-lib/asm/miden/tx.masm index 85d2a6500..11520887e 100644 --- a/miden-lib/asm/miden/tx.masm +++ b/miden-lib/asm/miden/tx.masm @@ -190,20 +190,24 @@ end #! than 15 elements back. Otherwise exceeding elements will not be provided to the procedure and #! will not be returned from it. #! -#! Inputs: [foreign_account_id, FOREIGN_PROC_ROOT, , pad(n)] +#! Inputs: [foreign_account_id_hi, foreign_account_id_lo, FOREIGN_PROC_ROOT, , pad(n)] #! Outputs: [] #! #! Where: +#! - foreign_account_id_{hi,lo} are the first and second felt of the account ID of the foreign +#! account to execute the procedure on. #! - pad(n) is the exact number of pads needed to set the number of procedure inputs to 16 at the #! moment of the foreign procedure execution (n = 16 - mem_addr_size - foreign_inputs_len). #! #! Invocation: exec export.execute_foreign_procedure.1 + # get the start_foreign_context procedure offset + push.0 movup.2 movup.2 exec.kernel_proc_offsets::start_foreign_context_offset + # => [offset, foreign_account_id_hi, foreign_account_id_lo, 0, FOREIGN_PROC_ROOT, , pad(n)] + # pad the stack before the syscall - push.0.0 movup.2 - exec.kernel_proc_offsets::start_foreign_context_offset padw swapw padw padw swapdw - # => [offset, foreign_account_id, pad(14), FOREIGN_PROC_ROOT, , pad(n)] + # => [offset, foreign_account_id_hi, foreign_account_id_lo, pad(13), FOREIGN_PROC_ROOT, , pad(n)] # load the foreign account to the memory syscall.exec_kernel_proc diff --git a/miden-lib/asm/note_scripts/P2ID.masm b/miden-lib/asm/note_scripts/P2ID.masm index db7888330..6bfe759bd 100644 --- a/miden-lib/asm/note_scripts/P2ID.masm +++ b/miden-lib/asm/note_scripts/P2ID.masm @@ -5,7 +5,7 @@ use.miden::contracts::wallets::basic->wallet # ERRORS # ================================================================================================= -# P2ID script expects exactly 1 note input +# P2ID script expects exactly 2 note inputs const.ERR_P2ID_WRONG_NUMBER_OF_INPUTS=0x00020050 # P2ID's target account address and transaction address do not match @@ -86,19 +86,19 @@ begin push.0 exec.note::get_inputs # => [num_inputs, inputs_ptr] - # make sure the number of inputs is 1 - eq.1 assert.err=ERR_P2ID_WRONG_NUMBER_OF_INPUTS + # make sure the number of inputs is 2 + eq.2 assert.err=ERR_P2ID_WRONG_NUMBER_OF_INPUTS # => [inputs_ptr] # read the target account id from the note inputs - mem_load - # => [target_account_id] + padw movup.4 mem_loadw drop drop + # => [target_account_id_hi, target_account_id_lo] exec.account::get_id - # => [account_id, target_account_id] + # => [account_id_hi, account_id_lo, target_account_id_hi, target_account_id_lo, ...] # ensure account_id = target_account_id, fails otherwise - assert_eq.err=ERR_P2ID_TARGET_ACCT_MISMATCH + exec.account::is_id_eq assert.err=ERR_P2ID_TARGET_ACCT_MISMATCH # => [] exec.add_note_assets_to_account diff --git a/miden-lib/asm/note_scripts/P2IDR.masm b/miden-lib/asm/note_scripts/P2IDR.masm index 7be5b62f4..1828b307d 100644 --- a/miden-lib/asm/note_scripts/P2IDR.masm +++ b/miden-lib/asm/note_scripts/P2IDR.masm @@ -6,7 +6,7 @@ use.miden::contracts::wallets::basic->wallet # ERRORS # ================================================================================================= -# P2IDR scripts expect exactly 2 note inputs +# P2IDR scripts expect exactly 3 note inputs const.ERR_P2IDR_WRONG_NUMBER_OF_INPUTS=0x00020052 # P2IDR's reclaimer is not the original sender @@ -95,32 +95,33 @@ begin push.0 exec.note::get_inputs # => [num_inputs, inputs_ptr] - # make sure the number of inputs is 2 - eq.2 assert.err=ERR_P2IDR_WRONG_NUMBER_OF_INPUTS + # make sure the number of inputs is 3 + eq.3 assert.err=ERR_P2IDR_WRONG_NUMBER_OF_INPUTS # => [inputs_ptr] # read the reclaim block height and target account id from the note inputs - padw movup.4 mem_loadw drop drop - # => [reclaim_block_height, target_account_id] + padw movup.4 mem_loadw drop + # => [reclaim_block_height, target_account_id_hi, target_account_id_lo] - exec.account::get_id dup - # => [account_id, account_id, reclaim_block_height, target_account_id] + exec.account::get_id dup.1 dup.1 + # => [account_id_hi, account_id_lo, account_id_hi, account_id_lo, reclaim_block_height, target_account_id_hi, target_account_id_lo, ...] # determine if the current account is the target account - movup.3 eq - # => [is_target, account_id, reclaim_block_height] + movup.6 movup.6 exec.account::is_id_eq + # => [is_target, account_id_hi, account_id_lo, reclaim_block_height] if.true # if current account is the target, we don't need to check anything else # and so we just clear the stack - drop drop + drop drop drop else # if current account is not the target, we need to ensure it is the sender exec.note::get_sender - # => [sender_account_id, account_id, reclaim_block_height] + # => [sender_account_id_hi, sender_account_id_lo, account_id_hi, account_id_lo, reclaim_block_height] - assert_eq.err=ERR_P2IDR_RECLAIM_ACCT_IS_NOT_SENDER + # ensure current account id = sender account id + exec.account::is_id_eq assert.err=ERR_P2IDR_RECLAIM_ACCT_IS_NOT_SENDER # => [reclaim_block_height] # now check that sender is allowed to reclaim, current block >= reclaim block height diff --git a/miden-lib/src/accounts/faucets/mod.rs b/miden-lib/src/accounts/faucets/mod.rs index 1435ffd17..d8a9bcf6c 100644 --- a/miden-lib/src/accounts/faucets/mod.rs +++ b/miden-lib/src/accounts/faucets/mod.rs @@ -1,6 +1,7 @@ use miden_objects::{ accounts::{ - Account, AccountBuilder, AccountComponent, AccountStorageMode, AccountType, StorageSlot, + Account, AccountBuilder, AccountComponent, AccountIdAnchor, AccountStorageMode, + AccountType, StorageSlot, }, assets::TokenSymbol, AccountError, Felt, FieldElement, Word, @@ -90,6 +91,7 @@ impl From for AccountComponent { /// - Slot 2: Token metadata of the faucet. pub fn create_basic_fungible_faucet( init_seed: [u8; 32], + id_anchor: AccountIdAnchor, symbol: TokenSymbol, decimals: u8, max_supply: Felt, @@ -104,6 +106,7 @@ pub fn create_basic_fungible_faucet( let (account, account_seed) = AccountBuilder::new() .init_seed(init_seed) + .anchor(id_anchor) .account_type(AccountType::FungibleFaucet) .storage_mode(account_storage_mode) .with_component(auth_component) @@ -118,7 +121,7 @@ pub fn create_basic_fungible_faucet( #[cfg(test)] mod tests { - use miden_objects::{crypto::dsa::rpo_falcon512, FieldElement, ONE}; + use miden_objects::{crypto::dsa::rpo_falcon512, digest, BlockHeader, FieldElement, ONE}; use vm_processor::Word; use super::{create_basic_fungible_faucet, AccountStorageMode, AuthScheme, Felt, TokenSymbol}; @@ -140,8 +143,17 @@ mod tests { let decimals = 2u8; let storage_mode = AccountStorageMode::Private; + let anchor_block_header_mock = BlockHeader::mock( + 0, + Some(digest!("0xaa")), + Some(digest!("0xbb")), + &[], + digest!("0xcc"), + ); + let (faucet_account, _) = create_basic_fungible_faucet( init_seed, + (&anchor_block_header_mock).try_into().unwrap(), token_symbol, decimals, max_supply, diff --git a/miden-lib/src/accounts/wallets/mod.rs b/miden-lib/src/accounts/wallets/mod.rs index 3ec6ea50b..849b83461 100644 --- a/miden-lib/src/accounts/wallets/mod.rs +++ b/miden-lib/src/accounts/wallets/mod.rs @@ -1,7 +1,9 @@ use alloc::string::ToString; use miden_objects::{ - accounts::{Account, AccountBuilder, AccountComponent, AccountStorageMode, AccountType}, + accounts::{ + Account, AccountBuilder, AccountComponent, AccountIdAnchor, AccountStorageMode, AccountType, + }, AccountError, Word, }; @@ -46,6 +48,7 @@ impl From for AccountComponent { /// authentication scheme. pub fn create_basic_wallet( init_seed: [u8; 32], + id_anchor: AccountIdAnchor, auth_scheme: AuthScheme, account_type: AccountType, account_storage_mode: AccountStorageMode, @@ -62,6 +65,7 @@ pub fn create_basic_wallet( let (account, account_seed) = AccountBuilder::new() .init_seed(init_seed) + .anchor(id_anchor) .account_type(account_type) .storage_mode(account_storage_mode) .with_component(auth_component) @@ -77,16 +81,25 @@ pub fn create_basic_wallet( #[cfg(test)] mod tests { - use miden_objects::{crypto::dsa::rpo_falcon512, ONE}; + use miden_objects::{crypto::dsa::rpo_falcon512, digest, BlockHeader, ONE}; use vm_processor::utils::{Deserializable, Serializable}; use super::{create_basic_wallet, Account, AccountStorageMode, AccountType, AuthScheme}; #[test] fn test_create_basic_wallet() { + let anchor_block_header_mock = BlockHeader::mock( + 0, + Some(digest!("0xaa")), + Some(digest!("0xbb")), + &[], + digest!("0xcc"), + ); + let pub_key = rpo_falcon512::PublicKey::new([ONE; 4]); let wallet = create_basic_wallet( [1; 32], + (&anchor_block_header_mock).try_into().unwrap(), AuthScheme::RpoFalcon512 { pub_key }, AccountType::RegularAccountImmutableCode, AccountStorageMode::Public, @@ -99,9 +112,18 @@ mod tests { #[test] fn test_serialize_basic_wallet() { + let anchor_block_header_mock = BlockHeader::mock( + 0, + Some(digest!("0xaa")), + Some(digest!("0xbb")), + &[], + digest!("0xcc"), + ); + let pub_key = rpo_falcon512::PublicKey::new([ONE; 4]); let wallet = create_basic_wallet( [1; 32], + (&anchor_block_header_mock).try_into().unwrap(), AuthScheme::RpoFalcon512 { pub_key }, AccountType::RegularAccountImmutableCode, AccountStorageMode::Public, diff --git a/miden-lib/src/errors/tx_kernel_errors.rs b/miden-lib/src/errors/tx_kernel_errors.rs index fdd2543ba..16fab03f6 100644 --- a/miden-lib/src/errors/tx_kernel_errors.rs +++ b/miden-lib/src/errors/tx_kernel_errors.rs @@ -10,18 +10,20 @@ // KERNEL ASSERTION ERROR // ================================================================================================ +pub const ERR_ACCOUNT_ANCHOR_BLOCK_HASH_MUST_NOT_BE_EMPTY: u32 = 0x00020007; pub const ERR_ACCOUNT_CODE_COMMITMENT_MISMATCH: u32 = 0x0002000F; pub const ERR_ACCOUNT_CODE_IS_NOT_UPDATABLE: u32 = 0x00020006; -pub const ERR_ACCOUNT_INSUFFICIENT_NUMBER_OF_ONES: u32 = 0x00020005; +pub const ERR_ACCOUNT_ID_EPOCH_MUST_BE_LESS_THAN_U16_MAX: u32 = 0x00020058; +pub const ERR_ACCOUNT_ID_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO: u32 = 0x00020005; +pub const ERR_ACCOUNT_ID_UNKNOWN_VERSION: u32 = 0x00020057; pub const ERR_ACCOUNT_INVALID_STORAGE_OFFSET_FOR_SIZE: u32 = 0x00020013; pub const ERR_ACCOUNT_IS_NOT_NATIVE: u32 = 0x00020030; pub const ERR_ACCOUNT_NONCE_DID_NOT_INCREASE_AFTER_STATE_CHANGE: u32 = 0x00020028; pub const ERR_ACCOUNT_NONCE_INCREASE_MUST_BE_U32: u32 = 0x00020004; -pub const ERR_ACCOUNT_POW_IS_INSUFFICIENT: u32 = 0x00020008; pub const ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS: u32 = 0x0002000C; pub const ERR_ACCOUNT_PROC_NOT_PART_OF_ACCOUNT_CODE: u32 = 0x0002000B; pub const ERR_ACCOUNT_READING_MAP_VALUE_FROM_NON_MAP_SLOT: u32 = 0x00020002; -pub const ERR_ACCOUNT_SEED_DIGEST_MISMATCH: u32 = 0x00020007; +pub const ERR_ACCOUNT_SEED_ANCHOR_BLOCK_HASH_DIGEST_MISMATCH: u32 = 0x00020008; pub const ERR_ACCOUNT_SETTING_MAP_ITEM_ON_NON_MAP_SLOT: u32 = 0x0002000A; pub const ERR_ACCOUNT_SETTING_VALUE_ITEM_ON_NON_VALUE_SLOT: u32 = 0x00020009; pub const ERR_ACCOUNT_STORAGE_COMMITMENT_MISMATCH: u32 = 0x00020012; @@ -48,8 +50,7 @@ pub const ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT: u32 = 0x0002004C pub const ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED: u32 = 0x0002004A; pub const ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN: u32 = 0x00020026; pub const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO: u32 = 0x00020020; -pub const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID: u32 = 0x00020022; -pub const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_MUST_BE_ZERO: u32 = 0x00020021; +pub const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_AND_THREE_MUST_BE_FUNGIBLE_FAUCET_ID: u32 = 0x00020022; pub const ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS: u32 = 0x00020023; pub const ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID: u32 = 0x0002004B; @@ -57,7 +58,7 @@ pub const ERR_KERNEL_PROCEDURE_OFFSET_OUT_OF_BOUNDS: u32 = 0x00020003; pub const ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS: u32 = 0x00020047; pub const ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN: u32 = 0x00020027; -pub const ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_FUNGIBLE_FAUCET_ID: u32 = 0x00020024; +pub const ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID: u32 = 0x00020024; pub const ERR_NON_FUNGIBLE_ASSET_FORMAT_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO: u32 = 0x00020025; pub const ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID: u32 = 0x0002004D; @@ -84,6 +85,7 @@ pub const ERR_PROLOGUE_GLOBAL_INPUTS_PROVIDED_DO_NOT_MATCH_BLOCK_HASH_COMMITMENT pub const ERR_PROLOGUE_INPUT_NOTES_COMMITMENT_MISMATCH: u32 = 0x00020041; pub const ERR_PROLOGUE_MISMATCH_OF_ACCOUNT_IDS_FROM_GLOBAL_INPUTS_AND_ADVICE_PROVIDER: u32 = 0x0002003C; pub const ERR_PROLOGUE_MISMATCH_OF_REFERENCE_BLOCK_MMR_AND_NOTE_AUTHENTICATION_MMR: u32 = 0x0002003D; +pub const ERR_PROLOGUE_NEW_ACCOUNT_NONCE_MUST_BE_ZERO: u32 = 0x0002005B; pub const ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY: u32 = 0x00020035; pub const ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_INVALID_TYPE: u32 = 0x00020037; pub const ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_EMPTY: u32 = 0x00020036; @@ -112,19 +114,21 @@ pub const ERR_VAULT_NON_FUNGIBLE_ASSET_ALREADY_EXISTS: u32 = 0x0002001C; pub const ERR_VAULT_NON_FUNGIBLE_ASSET_TO_REMOVE_NOT_FOUND: u32 = 0x0002001F; pub const ERR_VAULT_REMOVE_FUNGIBLE_ASSET_FAILED_INITIAL_VALUE_INVALID: u32 = 0x0002001E; -pub const TX_KERNEL_ERRORS: [(u32, &str); 87] = [ +pub const TX_KERNEL_ERRORS: [(u32, &str); 89] = [ + (ERR_ACCOUNT_ANCHOR_BLOCK_HASH_MUST_NOT_BE_EMPTY, "Anchor block hash must not be empty"), (ERR_ACCOUNT_CODE_COMMITMENT_MISMATCH, "Computed account code commitment does not match recorded account code commitment"), (ERR_ACCOUNT_CODE_IS_NOT_UPDATABLE, "Account code must be updatable for it to be possible to set new code"), - (ERR_ACCOUNT_INSUFFICIENT_NUMBER_OF_ONES, "Account ID must contain at least MIN_ACCOUNT_ONES number of ones"), + (ERR_ACCOUNT_ID_EPOCH_MUST_BE_LESS_THAN_U16_MAX, "Epoch must be less than u16::MAX (0xffff)."), + (ERR_ACCOUNT_ID_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO, "Least significant byte of second felt of the account id must be zero."), + (ERR_ACCOUNT_ID_UNKNOWN_VERSION, "Unknown version in account id."), (ERR_ACCOUNT_INVALID_STORAGE_OFFSET_FOR_SIZE, "Storage offset is invalid for 0 storage size (should be 0)"), (ERR_ACCOUNT_IS_NOT_NATIVE, "The current account is not native"), (ERR_ACCOUNT_NONCE_DID_NOT_INCREASE_AFTER_STATE_CHANGE, "Account nonce did not increase after a state changing transaction"), (ERR_ACCOUNT_NONCE_INCREASE_MUST_BE_U32, "Account nonce cannot be increased by a greater than u32 value"), - (ERR_ACCOUNT_POW_IS_INSUFFICIENT, "Account proof of work is insufficient"), (ERR_ACCOUNT_PROC_INDEX_OUT_OF_BOUNDS, "Provided procedure index is out of bounds"), (ERR_ACCOUNT_PROC_NOT_PART_OF_ACCOUNT_CODE, "Account procedure is not part of the account code"), (ERR_ACCOUNT_READING_MAP_VALUE_FROM_NON_MAP_SLOT, "Failed to read an account map item from a non-map storage slot"), - (ERR_ACCOUNT_SEED_DIGEST_MISMATCH, "ID of the new account does not match the ID computed from the seed"), + (ERR_ACCOUNT_SEED_ANCHOR_BLOCK_HASH_DIGEST_MISMATCH, "ID of the new account does not match the ID computed from the seed and anchor block hash"), (ERR_ACCOUNT_SETTING_MAP_ITEM_ON_NON_MAP_SLOT, "Failed to write an account map item to a non-map storage slot"), (ERR_ACCOUNT_SETTING_VALUE_ITEM_ON_NON_VALUE_SLOT, "Failed to write an account value item to a non-value storage slot"), (ERR_ACCOUNT_STORAGE_COMMITMENT_MISMATCH, "Computed account storage commitment does not match recorded account storage commitment"), @@ -151,8 +155,7 @@ pub const TX_KERNEL_ERRORS: [(u32, &str); 87] = [ (ERR_FUNGIBLE_ASSET_DISTRIBUTE_WOULD_CAUSE_MAX_SUPPLY_TO_BE_EXCEEDED, "Distribute would cause the maximum supply to be exceeded"), (ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN, "The origin of the fungible asset is not this faucet"), (ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_ZERO, "Malformed fungible asset: ASSET[1] must be 0"), - (ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID, "Malformed fungible asset: ASSET[3] must be a valide fungible faucet id"), - (ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_MUST_BE_ZERO, "Malformed fungible asset: ASSET[2] must be 0"), + (ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_TWO_AND_THREE_MUST_BE_FUNGIBLE_FAUCET_ID, "Malformed fungible asset: ASSET[2] and ASSET[3] must be a valid fungible faucet id"), (ERR_FUNGIBLE_ASSET_FORMAT_ELEMENT_ZERO_MUST_BE_WITHIN_LIMITS, "Malformed fungible asset: ASSET[0] exceeds the maximum allowed amount"), (ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID, "Failed to build the fungible asset because the provided faucet id is not from a fungible faucet"), @@ -160,7 +163,7 @@ pub const TX_KERNEL_ERRORS: [(u32, &str); 87] = [ (ERR_NON_FUNGIBLE_ASSET_ALREADY_EXISTS, "Non-fungible asset that already exists in the note cannot be added again"), (ERR_NON_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN, "The origin of the non-fungible asset is not this faucet"), - (ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_ONE_MUST_BE_FUNGIBLE_FAUCET_ID, "Malformed non-fungible asset: ASSET[1] is not a valid non-fungible faucet id"), + (ERR_NON_FUNGIBLE_ASSET_FORMAT_ELEMENT_THREE_MUST_BE_FUNGIBLE_FAUCET_ID, "Malformed non-fungible asset: ASSET[3] is not a valid non-fungible faucet id"), (ERR_NON_FUNGIBLE_ASSET_FORMAT_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO, "Malformed non-fungible asset: the most significant bit must be 0"), (ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID, "Failed to build the non-fungible asset because the provided faucet id is not from a non-fungible faucet"), @@ -177,16 +180,17 @@ pub const TX_KERNEL_ERRORS: [(u32, &str); 87] = [ (ERR_P2IDR_RECLAIM_ACCT_IS_NOT_SENDER, "P2IDR's reclaimer is not the original sender"), (ERR_P2IDR_RECLAIM_HEIGHT_NOT_REACHED, "P2IDR can not be reclaimed as the transaction's reference block is lower than the reclaim height"), - (ERR_P2IDR_WRONG_NUMBER_OF_INPUTS, "P2IDR scripts expect exactly 2 note inputs"), + (ERR_P2IDR_WRONG_NUMBER_OF_INPUTS, "P2IDR scripts expect exactly 3 note inputs"), (ERR_P2ID_TARGET_ACCT_MISMATCH, "P2ID's target account address and transaction address do not match"), - (ERR_P2ID_WRONG_NUMBER_OF_INPUTS, "P2ID script expects exactly 1 note input"), + (ERR_P2ID_WRONG_NUMBER_OF_INPUTS, "P2ID script expects exactly 2 note inputs"), (ERR_PROLOGUE_EXISTING_ACCOUNT_MUST_HAVE_NON_ZERO_NONCE, "Existing accounts must have a non-zero nonce"), (ERR_PROLOGUE_GLOBAL_INPUTS_PROVIDED_DO_NOT_MATCH_BLOCK_HASH_COMMITMENT, "The provided global inputs do not match the block hash commitment"), (ERR_PROLOGUE_INPUT_NOTES_COMMITMENT_MISMATCH, "Note commitment computed from the input note data does not match given note commitment"), (ERR_PROLOGUE_MISMATCH_OF_ACCOUNT_IDS_FROM_GLOBAL_INPUTS_AND_ADVICE_PROVIDER, "Account IDs provided via global inputs and advice provider do not match"), (ERR_PROLOGUE_MISMATCH_OF_REFERENCE_BLOCK_MMR_AND_NOTE_AUTHENTICATION_MMR, "Reference block MMR and note's authentication MMR must match"), + (ERR_PROLOGUE_NEW_ACCOUNT_NONCE_MUST_BE_ZERO, "New account must have a zero nonce"), (ERR_PROLOGUE_NEW_ACCOUNT_VAULT_MUST_BE_EMPTY, "New account must have an empty vault"), (ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_INVALID_TYPE, "Reserved slot for new fungible faucet has an invalid type"), (ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_EMPTY, "Reserved slot for new fungible faucet is not empty"), diff --git a/miden-lib/src/notes/mod.rs b/miden-lib/src/notes/mod.rs index e954828e5..8ef85bc78 100644 --- a/miden-lib/src/notes/mod.rs +++ b/miden-lib/src/notes/mod.rs @@ -36,15 +36,14 @@ pub fn create_p2id_note( aux: Felt, rng: &mut R, ) -> Result { - let note_script = scripts::p2id(); + let serial_num = rng.draw_word(); + let recipient = utils::build_p2id_recipient(target, serial_num)?; - let inputs = NoteInputs::new(vec![target.into()])?; let tag = NoteTag::from_account_id(target, NoteExecutionMode::Local)?; - let serial_num = rng.draw_word(); let metadata = NoteMetadata::new(sender, note_type, tag, NoteExecutionHint::always(), aux)?; let vault = NoteAssets::new(assets)?; - let recipient = NoteRecipient::new(serial_num, note_script, inputs); + Ok(Note::new(vault, metadata, recipient)) } @@ -71,7 +70,8 @@ pub fn create_p2idr_note( ) -> Result { let note_script = scripts::p2idr(); - let inputs = NoteInputs::new(vec![target.into(), recall_height.into()])?; + let inputs = + NoteInputs::new(vec![target.second_felt(), target.first_felt(), recall_height.into()])?; let tag = NoteTag::from_account_id(target, NoteExecutionMode::Local)?; let serial_num = rng.draw_word(); diff --git a/miden-lib/src/notes/utils.rs b/miden-lib/src/notes/utils.rs index 9a4539680..31cdf0946 100644 --- a/miden-lib/src/notes/utils.rs +++ b/miden-lib/src/notes/utils.rs @@ -1,12 +1,12 @@ use miden_objects::{ accounts::AccountId, assets::Asset, - notes::{NoteExecutionMode, NoteInputs, NoteRecipient, NoteScript, NoteTag, NoteType}, - utils::Deserializable, - vm::Program, + notes::{NoteExecutionMode, NoteInputs, NoteRecipient, NoteTag, NoteType}, NoteError, Word, }; +use crate::notes::scripts; + /// Creates a [NoteRecipient] for the P2ID note. /// /// Notes created with this recipient will be P2ID notes consumable by the specified target @@ -15,11 +15,8 @@ pub fn build_p2id_recipient( target: AccountId, serial_num: Word, ) -> Result { - let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/P2ID.masb")); - let program = - Program::read_from_bytes(bytes).map_err(NoteError::NoteScriptDeserializationError)?; - let note_script = NoteScript::new(program); - let note_inputs = NoteInputs::new(vec![target.into()])?; + let note_script = scripts::p2id(); + let note_inputs = NoteInputs::new(vec![target.second_felt(), target.first_felt()])?; Ok(NoteRecipient::new(serial_num, note_script, note_inputs)) } @@ -28,8 +25,8 @@ pub fn build_p2id_recipient( /// /// Use case ID for the returned tag is set to 0. /// -/// Tag payload is constructed by taking asset tags (8 bits of faucet ID) and concatenating them -/// together as offered_asset_tag + requested_asset tag. +/// Tag payload is constructed by taking asset tags (8 bits of each faucet ID) and concatenating +/// them together as offered_asset_tag + requested_asset tag. /// /// Network execution hint for the returned tag is set to `Local`. pub fn build_swap_tag( @@ -39,15 +36,12 @@ pub fn build_swap_tag( ) -> Result { const SWAP_USE_CASE_ID: u16 = 0; - // get bits 4..12 from faucet IDs of both assets, these bits will form the tag payload; the - // reason we skip the 4 most significant bits is that these encode metadata of underlying - // faucets and are likely to be the same for many different faucets. - - let offered_asset_id: u64 = offered_asset.faucet_id().into(); - let offered_asset_tag = (offered_asset_id >> 52) as u8; + // Get bits 0..8 from the faucet IDs of both assets which will form the tag payload. + let offered_asset_id: u64 = offered_asset.faucet_id_prefix().into(); + let offered_asset_tag = (offered_asset_id >> 56) as u8; - let requested_asset_id: u64 = requested_asset.faucet_id().into(); - let requested_asset_tag = (requested_asset_id >> 52) as u8; + let requested_asset_id: u64 = requested_asset.faucet_id_prefix().into(); + let requested_asset_tag = (requested_asset_id >> 56) as u8; let payload = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); @@ -57,3 +51,70 @@ pub fn build_swap_tag( _ => NoteTag::for_local_use_case(SWAP_USE_CASE_ID, payload), } } + +#[cfg(test)] +mod tests { + use miden_objects::{ + self, + accounts::{AccountStorageMode, AccountType}, + assets::{FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}, + }; + + use super::*; + + #[test] + fn swap_tag() { + // Construct an ID that starts with 0xcdb1. + let mut fungible_faucet_id_bytes = [0; 15]; + fungible_faucet_id_bytes[0] = 0xcd; + fungible_faucet_id_bytes[1] = 0xb1; + + // Construct an ID that starts with 0xabec. + let mut non_fungible_faucet_id_bytes = [0; 15]; + non_fungible_faucet_id_bytes[0] = 0xab; + non_fungible_faucet_id_bytes[1] = 0xec; + + let offered_asset = Asset::Fungible( + FungibleAsset::new( + AccountId::new_dummy( + fungible_faucet_id_bytes, + AccountType::FungibleFaucet, + AccountStorageMode::Public, + ), + 2500, + ) + .unwrap(), + ); + + let requested_asset = Asset::NonFungible( + NonFungibleAsset::new( + &NonFungibleAssetDetails::new( + AccountId::new_dummy( + non_fungible_faucet_id_bytes, + AccountType::NonFungibleFaucet, + AccountStorageMode::Public, + ) + .prefix(), + vec![0xaa, 0xbb, 0xcc, 0xdd], + ) + .unwrap(), + ) + .unwrap(), + ); + + // The fungible ID starts with 0xcdb1. + // The non fungible ID starts with 0xabec. + // The expected tag payload is thus 0xcdab. + let expected_tag_payload = 0xcdab; + + let actual_tag = + build_swap_tag(NoteType::Public, &offered_asset, &requested_asset).unwrap(); + + // 0 is the SWAP use case ID. + let expected_tag = + NoteTag::for_public_use_case(0, expected_tag_payload, NoteExecutionMode::Local) + .unwrap(); + + assert_eq!(actual_tag, expected_tag); + } +} diff --git a/miden-lib/src/transaction/inputs.rs b/miden-lib/src/transaction/inputs.rs index dc2a6965c..ad0b488a9 100644 --- a/miden-lib/src/transaction/inputs.rs +++ b/miden-lib/src/transaction/inputs.rs @@ -93,7 +93,12 @@ fn build_advice_stack( // push core account items onto the stack // Note: keep in sync with the process_account_data kernel procedure let account = tx_inputs.account(); - inputs.extend_stack([account.id().into(), ZERO, ZERO, account.nonce()]); + inputs.extend_stack([ + account.id().second_felt(), + account.id().first_felt(), + ZERO, + account.nonce(), + ]); inputs.extend_stack(account.vault().commitment()); inputs.extend_stack(account.storage().commitment()); inputs.extend_stack(account.code().commitment()); @@ -147,7 +152,7 @@ fn add_chain_mmr_to_advice_inputs(mmr: &ChainMmr, inputs: &mut AdviceInputs) { /// - The account storage commitment |-> storage slots and types vector. /// - The account code commitment |-> procedures vector. /// - The node |-> (key, value), for all leaf nodes of the asset vault SMT. -/// - [account_id, 0, 0, 0] |-> account_seed, when account seed is provided. +/// - [account_id_lo, account_id_hi, 0, 0] |-> account_seed, when account seed is provided. /// - If present, the Merkle leaves associated with the account storage maps. fn add_account_to_advice_inputs( account: &Account, @@ -189,7 +194,7 @@ fn add_account_to_advice_inputs( // --- account seed ------------------------------------------------------- if let Some(account_seed) = account_seed { inputs.extend_map(vec![( - [account.id().into(), ZERO, ZERO, ZERO].into(), + [account.id().second_felt(), account.id().first_felt(), ZERO, ZERO].into(), account_seed.to_vec(), )]); } diff --git a/miden-lib/src/transaction/memory.rs b/miden-lib/src/transaction/memory.rs index 7d810c26d..55f9cb3c5 100644 --- a/miden-lib/src/transaction/memory.rs +++ b/miden-lib/src/transaction/memory.rs @@ -185,7 +185,8 @@ pub const NATIVE_ACCT_ID_AND_NONCE_PTR: MemoryAddress = NATIVE_ACCOUNT_DATA_PTR + ACCT_ID_AND_NONCE_OFFSET; /// The index of the account id within the account id and nonce data. -pub const ACCT_ID_IDX: DataIndex = 0; +pub const ACCT_ID_LO_IDX: DataIndex = 0; +pub const ACCT_ID_HI_IDX: DataIndex = 1; /// The index of the account nonce within the account id and nonce data. pub const ACCT_NONCE_IDX: DataIndex = 3; diff --git a/miden-lib/src/transaction/mod.rs b/miden-lib/src/transaction/mod.rs index 01d10229c..3306270c6 100644 --- a/miden-lib/src/transaction/mod.rs +++ b/miden-lib/src/transaction/mod.rs @@ -144,16 +144,17 @@ impl TransactionKernel { /// - INITIAL_ACCOUNT_HASH, account state prior to the transaction, EMPTY_WORD for new accounts. /// - INPUT_NOTES_COMMITMENT, see `transaction::api::get_input_notes_commitment`. pub fn build_input_stack( - acct_id: AccountId, + account_id: AccountId, init_acct_hash: Digest, input_notes_hash: Digest, block_hash: Digest, ) -> StackInputs { // Note: Must be kept in sync with the transaction's kernel prepare_transaction procedure - let mut inputs: Vec = Vec::with_capacity(13); + let mut inputs: Vec = Vec::with_capacity(14); inputs.extend(input_notes_hash); inputs.extend_from_slice(init_acct_hash.as_elements()); - inputs.push(acct_id.into()); + inputs.push(account_id.second_felt()); + inputs.push(account_id.first_felt()); inputs.extend_from_slice(block_hash.as_elements()); StackInputs::new(inputs) .map_err(|e| e.to_string()) @@ -178,7 +179,8 @@ impl TransactionKernel { let account_id = account_header.id(); let storage_root = account_header.storage_commitment(); let code_root = account_header.code_commitment(); - let account_key = Digest::from([account_id.into(), ZERO, ZERO, ZERO]); + let account_key = + Digest::from([account_id.first_felt(), account_id.second_felt(), ZERO, ZERO]); // Extend the advice inputs with the new data advice_inputs.extend_map([ @@ -192,7 +194,8 @@ impl TransactionKernel { // Extend the advice inputs with Merkle store data advice_inputs.extend_merkle_store( - merkle_path.inner_nodes(account_id.into(), account_header.hash())?, + // The first felt is the index in the account tree. + merkle_path.inner_nodes(account_id.first_felt().as_int(), account_header.hash())?, ); Ok(()) diff --git a/miden-lib/src/transaction/outputs.rs b/miden-lib/src/transaction/outputs.rs index adfc016d3..ac16a7ed8 100644 --- a/miden-lib/src/transaction/outputs.rs +++ b/miden-lib/src/transaction/outputs.rs @@ -4,9 +4,10 @@ use miden_objects::{ }; use super::memory::{ - ACCT_CODE_COMMITMENT_OFFSET, ACCT_DATA_MEM_SIZE, ACCT_ID_AND_NONCE_OFFSET, ACCT_ID_IDX, + ACCT_CODE_COMMITMENT_OFFSET, ACCT_DATA_MEM_SIZE, ACCT_ID_AND_NONCE_OFFSET, ACCT_ID_HI_IDX, ACCT_NONCE_IDX, ACCT_STORAGE_COMMITMENT_OFFSET, ACCT_VAULT_ROOT_OFFSET, }; +use crate::transaction::memory::ACCT_ID_LO_IDX; // STACK OUTPUTS // ================================================================================================ @@ -33,7 +34,10 @@ pub fn parse_final_account_header(elements: &[Word]) -> Result Self { - self.exec_options = ExecutionOptions::new( - Some(self.exec_options.max_cycles()), - self.exec_options.expected_cycles(), - self.exec_options.enable_tracing(), - in_debug_mode, - ) - .expect("failed to clone execution options"); - + pub fn with_debug_mode(mut self) -> Self { + self.exec_options = self.exec_options.with_debugging(); self } diff --git a/miden-tx/src/host/mod.rs b/miden-tx/src/host/mod.rs index f1cc04ec1..f0951b185 100644 --- a/miden-tx/src/host/mod.rs +++ b/miden-tx/src/host/mod.rs @@ -88,7 +88,7 @@ pub struct TransactionHost { /// Contains mappings from error codes to the related error messages. /// - /// This map is initialized at construction time from the [KERNEL_ERRORS] array. + /// This map is initialized at construction time from the [`TX_KERNEL_ERRORS`] array. error_messages: BTreeMap, } diff --git a/miden-tx/src/testing/mock_chain/mod.rs b/miden-tx/src/testing/mock_chain/mod.rs index 1e77ede03..34a3f8a78 100644 --- a/miden-tx/src/testing/mock_chain/mod.rs +++ b/miden-tx/src/testing/mock_chain/mod.rs @@ -8,10 +8,13 @@ use miden_lib::{ use miden_objects::{ accounts::{ delta::AccountUpdateDetails, Account, AccountBuilder, AccountComponent, AccountDelta, - AccountId, AccountType, AuthSecretKey, + AccountId, AccountIdAnchor, AccountType, AuthSecretKey, }, assets::{Asset, FungibleAsset, TokenSymbol}, - block::{compute_tx_hash, Block, BlockAccountUpdate, BlockNoteIndex, BlockNoteTree, NoteBatch}, + block::{ + block_num_from_epoch, compute_tx_hash, Block, BlockAccountUpdate, BlockNoteIndex, + BlockNoteTree, NoteBatch, + }, crypto::{ dsa::rpo_falcon512::SecretKey, merkle::{Mmr, MmrError, PartialMmr, Smt}, @@ -507,6 +510,10 @@ impl MockChain { }; let (account, seed) = if let AccountState::New = account_state { + let last_block = self.blocks.last().expect("one block should always exist"); + account_builder = + account_builder.anchor(AccountIdAnchor::try_from(&last_block.header()).unwrap()); + account_builder.build().map(|(account, seed)| (account, Some(seed))).unwrap() } else { account_builder.build_existing().map(|account| (account, None)).unwrap() @@ -570,7 +577,7 @@ impl MockChain { /// Returns a valid [TransactionInputs] for the specified entities. pub fn get_transaction_inputs( - &mut self, + &self, account: Account, account_seed: Option, notes: &[NoteId], @@ -592,6 +599,20 @@ impl MockChain { input_notes.push(input_note); } + // If the account is new, add the anchor block's header from which the account ID is derived + // to the MMR. + if account.is_new() { + let epoch_block_num = block_num_from_epoch(account.id().anchor_epoch()); + // The reference block of the transaction is added to the MMR in + // prologue::process_chain_data so we can skip adding it to the block headers here. + if epoch_block_num != block.header().block_num() { + block_headers_map.insert( + epoch_block_num, + self.blocks.get(epoch_block_num as usize).unwrap().header(), + ); + } + } + for note in unauthenticated_notes { input_notes.push(InputNote::Unauthenticated { note: note.clone() }) } diff --git a/miden-tx/src/testing/mock_host.rs b/miden-tx/src/testing/mock_host.rs index 91ec89998..606b50bff 100644 --- a/miden-tx/src/testing/mock_host.rs +++ b/miden-tx/src/testing/mock_host.rs @@ -1,6 +1,6 @@ -use alloc::{rc::Rc, string::ToString, sync::Arc, vec::Vec}; +use alloc::{collections::BTreeMap, rc::Rc, string::ToString, sync::Arc, vec::Vec}; -use miden_lib::transaction::TransactionEvent; +use miden_lib::{errors::tx_kernel_errors::TX_KERNEL_ERRORS, transaction::TransactionEvent}; use miden_objects::{ accounts::{AccountHeader, AccountVaultDelta}, Digest, @@ -24,6 +24,10 @@ pub struct MockHost { adv_provider: MemAdviceProvider, acct_procedure_index_map: AccountProcedureIndexMap, mast_store: Rc, + /// Contains mappings from error codes to the related error messages. + /// + /// This map is initialized at construction time from the [`TX_KERNEL_ERRORS`] array. + error_messages: BTreeMap, } impl MockHost { @@ -37,10 +41,14 @@ impl MockHost { foreign_code_commitments.push(account.code_commitment()); let adv_provider: MemAdviceProvider = advice_inputs.into(); let proc_index_map = AccountProcedureIndexMap::new(foreign_code_commitments, &adv_provider); + + let kernel_assertion_errors = BTreeMap::from(TX_KERNEL_ERRORS); + Self { adv_provider, acct_procedure_index_map: proc_index_map.unwrap(), mast_store, + error_messages: kernel_assertion_errors, } } @@ -109,4 +117,18 @@ impl Host for MockHost { Ok(HostResponse::None) } + + fn on_assert_failed(&mut self, process: &S, err_code: u32) -> ExecutionError { + let err_msg = self + .error_messages + .get(&err_code) + .map_or("Unknown error".to_string(), |msg| msg.to_string()); + // Add hex representation to message so it can be easily found in MASM code. + let err_msg = format!("0x{:08X}: {}", err_code, err_msg); + ExecutionError::FailedAssertion { + clk: process.clk(), + err_code, + err_msg: Some(err_msg), + } + } } diff --git a/miden-tx/src/testing/tx_context/builder.rs b/miden-tx/src/testing/tx_context/builder.rs index c5df57d49..5ed6ca287 100644 --- a/miden-tx/src/testing/tx_context/builder.rs +++ b/miden-tx/src/testing/tx_context/builder.rs @@ -5,18 +5,16 @@ use alloc::{collections::BTreeMap, vec::Vec}; use miden_lib::transaction::TransactionKernel; use miden_objects::{ - accounts::{ - account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, - ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, ACCOUNT_ID_SENDER, - }, - Account, AccountCode, AccountId, - }, + accounts::{Account, AccountCode, AccountId}, assembly::Assembler, assets::{Asset, FungibleAsset, NonFungibleAsset}, notes::{Note, NoteExecutionHint, NoteId, NoteType}, testing::{ + account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, ACCOUNT_ID_SENDER, + }, constants::{ CONSUMED_ASSET_1_AMOUNT, CONSUMED_ASSET_2_AMOUNT, CONSUMED_ASSET_3_AMOUNT, NON_FUNGIBLE_ASSET_DATA_2, @@ -130,7 +128,7 @@ impl TransactionContextBuilder { } /// Initializes a [TransactionContextBuilder] with a mocked fungible faucet. - pub fn with_fungible_faucet(acct_id: u64, nonce: Felt, initial_balance: Felt) -> Self { + pub fn with_fungible_faucet(acct_id: u128, nonce: Felt, initial_balance: Felt) -> Self { let account = Account::mock_fungible_faucet( acct_id, nonce, @@ -142,7 +140,7 @@ impl TransactionContextBuilder { } /// Initializes a [TransactionContextBuilder] with a mocked non-fungible faucet. - pub fn with_non_fungible_faucet(acct_id: u64, nonce: Felt, empty_reserved_slot: bool) -> Self { + pub fn with_non_fungible_faucet(acct_id: u128, nonce: Felt, empty_reserved_slot: bool) -> Self { let account = Account::mock_non_fungible_faucet( acct_id, nonce, diff --git a/miden-tx/src/testing/tx_context/mod.rs b/miden-tx/src/testing/tx_context/mod.rs index 88317f748..e3cd17e9c 100644 --- a/miden-tx/src/testing/tx_context/mod.rs +++ b/miden-tx/src/testing/tx_context/mod.rs @@ -64,7 +64,7 @@ impl TransactionContext { let test_lib = TransactionKernel::kernel_as_library(); mast_store.insert(test_lib.mast_forest().clone()); - let program = self.assembler.clone().assemble_program(code).unwrap(); + let program = self.assembler.clone().with_debug_mode(true).assemble_program(code).unwrap(); mast_store.insert(program.mast_forest().clone()); for code in &self.foreign_codes { @@ -94,6 +94,7 @@ impl TransactionContext { let authenticator = self .authenticator .map(|auth| Arc::new(auth) as Arc); + let mut tx_executor = TransactionExecutor::new(Arc::new(self.tx_inputs), authenticator); for code in self.foreign_codes { diff --git a/miden-tx/src/tests/kernel_tests/mod.rs b/miden-tx/src/tests/kernel_tests/mod.rs index 668d50b83..e5f278bad 100644 --- a/miden-tx/src/tests/kernel_tests/mod.rs +++ b/miden-tx/src/tests/kernel_tests/mod.rs @@ -36,7 +36,7 @@ macro_rules! assert_execution_error { ); }, Ok(_) => panic!("Execution was unexpectedly successful"), - Err(_) => panic!("Execution error was not as expected"), + Err(err) => panic!("Execution error was not as expected: {err}"), } }; } diff --git a/miden-tx/src/tests/kernel_tests/test_account.rs b/miden-tx/src/tests/kernel_tests/test_account.rs index 4f9428770..aaf328f6d 100644 --- a/miden-tx/src/tests/kernel_tests/test_account.rs +++ b/miden-tx/src/tests/kernel_tests/test_account.rs @@ -1,23 +1,23 @@ -use miden_lib::{ - errors::tx_kernel_errors::ERR_ACCOUNT_INSUFFICIENT_NUMBER_OF_ONES, - transaction::{ - memory::{NATIVE_ACCT_CODE_COMMITMENT_PTR, NEW_CODE_ROOT_PTR}, - TransactionKernel, - }, +use miden_lib::transaction::{ + memory::{NATIVE_ACCT_CODE_COMMITMENT_PTR, NEW_CODE_ROOT_PTR}, + TransactionKernel, }; use miden_objects::{ accounts::{ - account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_INSUFFICIENT_ONES, - ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, - ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, - ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, - }, AccountBuilder, AccountCode, AccountComponent, AccountId, AccountStorage, AccountType, StorageSlot, }, assembly::Library, - testing::{account_component::AccountMockComponent, prepare_word, storage::STORAGE_LEAVES_2}, + testing::{ + account_component::AccountMockComponent, + account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + }, + prepare_word, + storage::STORAGE_LEAVES_2, + }, transaction::TransactionScript, }; use rand::{Rng, SeedableRng}; @@ -26,7 +26,6 @@ use vm_processor::{Digest, MemAdviceProvider, ProcessState}; use super::{Felt, StackInputs, Word, ONE, ZERO}; use crate::{ - assert_execution_error, testing::{executor::CodeExecutor, TransactionContextBuilder}, tests::kernel_tests::{output_notes_data_procedure, read_root_mem_value}, }; @@ -143,7 +142,7 @@ pub fn test_account_type() { ); let process = CodeExecutor::with_advice_provider(MemAdviceProvider::default()) - .stack_inputs(StackInputs::new(vec![account_id.into()]).unwrap()) + .stack_inputs(StackInputs::new(vec![account_id.first_felt()]).unwrap()) .run(&code) .unwrap(); @@ -166,24 +165,6 @@ pub fn test_account_type() { } } -#[test] -fn test_validate_id_fails_on_insufficient_ones() { - let code = format!( - " - use.kernel::account - - begin - push.{ACCOUNT_ID_INSUFFICIENT_ONES} - exec.account::validate_id - end - " - ); - - let result = CodeExecutor::with_advice_provider(MemAdviceProvider::default()).run(&code); - - assert_execution_error!(result, ERR_ACCOUNT_INSUFFICIENT_NUMBER_OF_ONES); -} - #[test] fn test_is_faucet_procedure() { let test_cases = [ @@ -201,14 +182,15 @@ fn test_is_faucet_procedure() { use.kernel::account begin - push.{account_id} + push.{first_felt} exec.account::is_faucet + # => [is_faucet, account_id_hi] # truncate the stack swap drop end ", - account_id = account_id, + first_felt = account_id.first_felt(), ); let process = CodeExecutor::with_advice_provider(MemAdviceProvider::default()) diff --git a/miden-tx/src/tests/kernel_tests/test_asset.rs b/miden-tx/src/tests/kernel_tests/test_asset.rs index 1f54d0930..a9dfce990 100644 --- a/miden-tx/src/tests/kernel_tests/test_asset.rs +++ b/miden-tx/src/tests/kernel_tests/test_asset.rs @@ -1,7 +1,8 @@ use miden_objects::{ - accounts::account_id::testing::ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + accounts::AccountId, assets::NonFungibleAsset, testing::{ + account_id::ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, constants::{ FUNGIBLE_ASSET_AMOUNT, FUNGIBLE_FAUCET_INITIAL_BALANCE, NON_FUNGIBLE_ASSET_DATA, }, @@ -42,13 +43,14 @@ fn test_create_fungible_asset_succeeds() { let process = tx_context.execute_code(&code).unwrap(); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); assert_eq!( process.get_stack_word(0), Word::from([ Felt::new(FUNGIBLE_ASSET_AMOUNT), Felt::new(0), - Felt::new(0), - Felt::new(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN) + faucet_id.second_felt(), + faucet_id.first_felt(), ]) ); } diff --git a/miden-tx/src/tests/kernel_tests/test_asset_vault.rs b/miden-tx/src/tests/kernel_tests/test_asset_vault.rs index 81070cd3c..8ad83bbfb 100644 --- a/miden-tx/src/tests/kernel_tests/test_asset_vault.rs +++ b/miden-tx/src/tests/kernel_tests/test_asset_vault.rs @@ -10,15 +10,13 @@ use miden_lib::{ transaction::memory, }; use miden_objects::{ - accounts::{ - account_id::testing::{ + accounts::AccountId, + assets::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}, + testing::{ + account_id::{ ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, }, - AccountId, - }, - assets::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}, - testing::{ constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}, prepare_word, }, @@ -43,13 +41,15 @@ fn test_get_balance() { begin exec.prologue::prepare_transaction - push.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN} + push.{second_felt}.{first_felt} exec.account::get_balance # truncate the stack swap drop end - " + ", + first_felt = faucet_id.first_felt(), + second_felt = faucet_id.second_felt(), ); let process = tx_context.execute_code(&code).unwrap(); @@ -63,6 +63,8 @@ fn test_get_balance() { #[test] fn test_get_balance_non_fungible_fails() { let tx_context = TransactionContextBuilder::with_standard_account(ONE).build(); + + let faucet_id = AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); let code = format!( " use.kernel::prologue @@ -70,10 +72,12 @@ fn test_get_balance_non_fungible_fails() { begin exec.prologue::prepare_transaction - push.{ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN} + push.{second_felt}.{first_felt} exec.account::get_balance end - " + ", + first_felt = faucet_id.first_felt(), + second_felt = faucet_id.second_felt(), ); let process = tx_context.execute_code(&code); @@ -87,7 +91,8 @@ fn test_get_balance_non_fungible_fails() { #[test] fn test_has_non_fungible_asset() { let tx_context = TransactionContextBuilder::with_standard_account(ONE).build(); - let non_fungible_asset = tx_context.account().vault().assets().next().unwrap(); + let non_fungible_asset = + tx_context.account().vault().assets().find(Asset::is_non_fungible).unwrap(); let code = format!( " @@ -103,7 +108,7 @@ fn test_has_non_fungible_asset() { swap drop end ", - non_fungible_asset_key = prepare_word(&non_fungible_asset.vault_key()) + non_fungible_asset_key = prepare_word(&non_fungible_asset.into()) ); let process = tx_context.execute_code(&code).unwrap(); @@ -118,7 +123,8 @@ fn test_add_fungible_asset_success() { let faucet_id: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(); let amount = FungibleAsset::MAX_AMOUNT - FUNGIBLE_ASSET_AMOUNT; let add_fungible_asset = - Asset::try_from([Felt::new(amount), ZERO, ZERO, faucet_id.into()]).unwrap(); + Asset::try_from([Felt::new(amount), ZERO, faucet_id.second_felt(), faucet_id.first_felt()]) + .unwrap(); let code = format!( " @@ -158,7 +164,8 @@ fn test_add_non_fungible_asset_fail_overflow() { let faucet_id: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(); let amount = FungibleAsset::MAX_AMOUNT - FUNGIBLE_ASSET_AMOUNT + 1; let add_fungible_asset = - Asset::try_from([Felt::new(amount), ZERO, ZERO, faucet_id.into()]).unwrap(); + Asset::try_from([Felt::new(amount), ZERO, faucet_id.second_felt(), faucet_id.first_felt()]) + .unwrap(); let code = format!( " @@ -187,7 +194,8 @@ fn test_add_non_fungible_asset_success() { let mut account_vault = tx_context.account().vault().clone(); let add_non_fungible_asset = Asset::NonFungible( NonFungibleAsset::new( - &NonFungibleAssetDetails::new(faucet_id, vec![1, 2, 3, 4, 5, 6, 7, 8]).unwrap(), + &NonFungibleAssetDetails::new(faucet_id.prefix(), vec![1, 2, 3, 4, 5, 6, 7, 8]) + .unwrap(), ) .unwrap(), ); @@ -228,7 +236,7 @@ fn test_add_non_fungible_asset_fail_duplicate() { let faucet_id: AccountId = ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(); let mut account_vault = tx_context.account().vault().clone(); let non_fungible_asset_details = - NonFungibleAssetDetails::new(faucet_id, NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); + NonFungibleAssetDetails::new(faucet_id.prefix(), NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); let non_fungible_asset = Asset::NonFungible(NonFungibleAsset::new(&non_fungible_asset_details).unwrap()); @@ -260,7 +268,8 @@ fn test_remove_fungible_asset_success_no_balance_remaining() { let faucet_id: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(); let amount = FUNGIBLE_ASSET_AMOUNT; let remove_fungible_asset = - Asset::try_from([Felt::new(amount), ZERO, ZERO, faucet_id.into()]).unwrap(); + Asset::try_from([Felt::new(amount), ZERO, faucet_id.second_felt(), faucet_id.first_felt()]) + .unwrap(); let code = format!( " @@ -298,7 +307,8 @@ fn test_remove_fungible_asset_fail_remove_too_much() { let faucet_id: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(); let amount = FUNGIBLE_ASSET_AMOUNT + 1; let remove_fungible_asset = - Asset::try_from([Felt::new(amount), ZERO, ZERO, faucet_id.into()]).unwrap(); + Asset::try_from([Felt::new(amount), ZERO, faucet_id.second_felt(), faucet_id.first_felt()]) + .unwrap(); let code = format!( " @@ -327,7 +337,8 @@ fn test_remove_fungible_asset_success_balance_remaining() { let faucet_id: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(); let amount = FUNGIBLE_ASSET_AMOUNT - 1; let remove_fungible_asset = - Asset::try_from([Felt::new(amount), ZERO, ZERO, faucet_id.into()]).unwrap(); + Asset::try_from([Felt::new(amount), ZERO, faucet_id.second_felt(), faucet_id.first_felt()]) + .unwrap(); let code = format!( " @@ -366,7 +377,7 @@ fn test_remove_inexisting_non_fungible_asset_fails() { let mut account_vault = tx_context.account().vault().clone(); let non_fungible_asset_details = - NonFungibleAssetDetails::new(faucet_id, NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); + NonFungibleAssetDetails::new(faucet_id.prefix(), NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); let nonfungible = NonFungibleAsset::new(&non_fungible_asset_details).unwrap(); let non_existent_non_fungible_asset = Asset::NonFungible(nonfungible); @@ -406,7 +417,7 @@ fn test_remove_non_fungible_asset_success() { let faucet_id: AccountId = ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(); let mut account_vault = tx_context.account().vault().clone(); let non_fungible_asset_details = - NonFungibleAssetDetails::new(faucet_id, NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); + NonFungibleAssetDetails::new(faucet_id.prefix(), NON_FUNGIBLE_ASSET_DATA.to_vec()).unwrap(); let non_fungible_asset = Asset::NonFungible(NonFungibleAsset::new(&non_fungible_asset_details).unwrap()); diff --git a/miden-tx/src/tests/kernel_tests/test_faucet.rs b/miden-tx/src/tests/kernel_tests/test_faucet.rs index a7e02a675..9a0f7406b 100644 --- a/miden-tx/src/tests/kernel_tests/test_faucet.rs +++ b/miden-tx/src/tests/kernel_tests/test_faucet.rs @@ -10,12 +10,13 @@ use miden_lib::{ transaction::memory::NATIVE_ACCT_STORAGE_SLOTS_SECTION_PTR, }; use miden_objects::{ - accounts::account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, - ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, - }, + accounts::AccountId, assets::{FungibleAsset, NonFungibleAsset}, testing::{ + account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, + ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, + }, constants::{ CONSUMED_ASSET_1_AMOUNT, FUNGIBLE_ASSET_AMOUNT, FUNGIBLE_FAUCET_INITIAL_BALANCE, NON_FUNGIBLE_ASSET_DATA, NON_FUNGIBLE_ASSET_DATA_2, @@ -42,6 +43,8 @@ fn test_mint_fungible_asset_succeeds() { ) .build(); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + let code = format!( " use.test::account @@ -52,20 +55,22 @@ fn test_mint_fungible_asset_succeeds() { begin # mint asset exec.prologue::prepare_transaction - push.{FUNGIBLE_ASSET_AMOUNT}.0.0.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN} + push.{FUNGIBLE_ASSET_AMOUNT}.0.{second_felt}.{first_felt} call.account::mint # assert the correct asset is returned - push.{FUNGIBLE_ASSET_AMOUNT}.0.0.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN} + push.{FUNGIBLE_ASSET_AMOUNT}.0.{second_felt}.{first_felt} assert_eqw # assert the input vault has been updated exec.memory::get_input_vault_root_ptr - push.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN} + push.{second_felt}.{first_felt} exec.asset_vault::get_balance push.{FUNGIBLE_ASSET_AMOUNT} assert_eq end - " + ", + first_felt = faucet_id.first_felt(), + second_felt = faucet_id.second_felt(), ); let process = tx_context.execute_code(&code).unwrap(); @@ -85,6 +90,8 @@ fn test_mint_fungible_asset_succeeds() { fn test_mint_fungible_asset_fails_not_faucet_account() { let tx_context = TransactionContextBuilder::with_standard_account(ONE).build(); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + let code = format!( " use.kernel::prologue @@ -92,10 +99,12 @@ fn test_mint_fungible_asset_fails_not_faucet_account() { begin exec.prologue::prepare_transaction - push.{FUNGIBLE_ASSET_AMOUNT}.0.0.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN} + push.{FUNGIBLE_ASSET_AMOUNT}.0.{second_felt}.{first_felt} call.account::mint end - " + ", + first_felt = faucet_id.first_felt(), + second_felt = faucet_id.second_felt(), ); let process = tx_context.execute_code(&code); @@ -107,6 +116,7 @@ fn test_mint_fungible_asset_fails_not_faucet_account() { fn test_mint_fungible_asset_inconsistent_faucet_id() { let tx_context = TransactionContextBuilder::with_standard_account(ONE).build(); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1).unwrap(); let code = format!( " use.kernel::prologue @@ -114,10 +124,12 @@ fn test_mint_fungible_asset_inconsistent_faucet_id() { begin exec.prologue::prepare_transaction - push.{FUNGIBLE_ASSET_AMOUNT}.0.0.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1} + push.{FUNGIBLE_ASSET_AMOUNT}.0.{second_felt}.{first_felt} call.account::mint end ", + first_felt = faucet_id.first_felt(), + second_felt = faucet_id.second_felt(), ); let process = tx_context.execute_code(&code); @@ -134,6 +146,7 @@ fn test_mint_fungible_asset_fails_saturate_max_amount() { ) .build(); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); let code = format!( " use.kernel::prologue @@ -141,10 +154,12 @@ fn test_mint_fungible_asset_fails_saturate_max_amount() { begin exec.prologue::prepare_transaction - push.{saturating_amount}.0.0.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN} + push.{saturating_amount}.0.{second_felt}.{first_felt} call.account::mint end ", + first_felt = faucet_id.first_felt(), + second_felt = faucet_id.second_felt(), saturating_amount = FungibleAsset::MAX_AMOUNT - FUNGIBLE_FAUCET_INITIAL_BALANCE + 1 ); @@ -303,6 +318,8 @@ fn test_burn_fungible_asset_succeeds() { .with_mock_notes_preserved() .build(); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1).unwrap(); + let code = format!( " use.test::account @@ -313,21 +330,23 @@ fn test_burn_fungible_asset_succeeds() { begin # mint asset exec.prologue::prepare_transaction - push.{FUNGIBLE_ASSET_AMOUNT}.0.0.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1} + push.{FUNGIBLE_ASSET_AMOUNT}.0.{second_felt}.{first_felt} call.account::burn # assert the correct asset is returned - push.{FUNGIBLE_ASSET_AMOUNT}.0.0.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1} + push.{FUNGIBLE_ASSET_AMOUNT}.0.{second_felt}.{first_felt} assert_eqw # assert the input vault has been updated exec.memory::get_input_vault_root_ptr - push.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1} + push.{second_felt}.{first_felt} exec.asset_vault::get_balance push.{final_input_vault_asset_amount} assert_eq end ", + first_felt = faucet_id.first_felt(), + second_felt = faucet_id.second_felt(), final_input_vault_asset_amount = CONSUMED_ASSET_1_AMOUNT - FUNGIBLE_ASSET_AMOUNT, ); @@ -348,6 +367,8 @@ fn test_burn_fungible_asset_succeeds() { fn test_burn_fungible_asset_fails_not_faucet_account() { let tx_context = TransactionContextBuilder::with_standard_account(ONE).build(); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1).unwrap(); + let code = format!( " use.kernel::prologue @@ -355,10 +376,12 @@ fn test_burn_fungible_asset_fails_not_faucet_account() { begin exec.prologue::prepare_transaction - push.{FUNGIBLE_ASSET_AMOUNT}.0.0.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1} + push.{FUNGIBLE_ASSET_AMOUNT}.0.{second_felt}.{first_felt} call.account::burn end - " + ", + first_felt = faucet_id.first_felt(), + second_felt = faucet_id.second_felt(), ); let process = tx_context.execute_code(&code); @@ -375,6 +398,8 @@ fn test_burn_fungible_asset_inconsistent_faucet_id() { ) .build(); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1).unwrap(); + let code = format!( " use.kernel::prologue @@ -382,10 +407,12 @@ fn test_burn_fungible_asset_inconsistent_faucet_id() { begin exec.prologue::prepare_transaction - push.{FUNGIBLE_ASSET_AMOUNT}.0.0.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1} + push.{FUNGIBLE_ASSET_AMOUNT}.0.{second_felt}.{first_felt} call.account::burn end ", + first_felt = faucet_id.first_felt(), + second_felt = faucet_id.second_felt(), ); let process = tx_context.execute_code(&code); @@ -402,6 +429,8 @@ fn test_burn_fungible_asset_insufficient_input_amount() { ) .build(); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1).unwrap(); + let code = format!( " use.kernel::prologue @@ -409,10 +438,12 @@ fn test_burn_fungible_asset_insufficient_input_amount() { begin exec.prologue::prepare_transaction - push.{saturating_amount}.0.0.{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1} + push.{saturating_amount}.0.{second_felt}.{first_felt} call.account::burn end ", + first_felt = faucet_id.first_felt(), + second_felt = faucet_id.second_felt(), saturating_amount = CONSUMED_ASSET_1_AMOUNT + 1 ); diff --git a/miden-tx/src/tests/kernel_tests/test_note.rs b/miden-tx/src/tests/kernel_tests/test_note.rs index 62d40e06b..8374cc6c6 100644 --- a/miden-tx/src/tests/kernel_tests/test_note.rs +++ b/miden-tx/src/tests/kernel_tests/test_note.rs @@ -62,14 +62,15 @@ fn test_get_sender() { exec.note::get_sender # truncate the stack - swap drop + swapw dropw end "; let process = tx_context.execute_code(code).unwrap(); - let sender = tx_context.input_notes().get_note(0).note().metadata().sender().into(); - assert_eq!(process.stack.get(0), sender); + let sender = tx_context.input_notes().get_note(0).note().metadata().sender(); + assert_eq!(process.stack.get(0), sender.first_felt()); + assert_eq!(process.stack.get(1), sender.second_felt()); } #[test] diff --git a/miden-tx/src/tests/kernel_tests/test_prologue.rs b/miden-tx/src/tests/kernel_tests/test_prologue.rs index 7146d86fd..dae5777cf 100644 --- a/miden-tx/src/tests/kernel_tests/test_prologue.rs +++ b/miden-tx/src/tests/kernel_tests/test_prologue.rs @@ -1,9 +1,10 @@ -use alloc::collections::BTreeMap; +use alloc::{collections::BTreeMap, vec::Vec}; +use anyhow::Context; use miden_lib::{ accounts::wallets::BasicWallet, errors::tx_kernel_errors::{ - ERR_ACCOUNT_SEED_DIGEST_MISMATCH, + ERR_ACCOUNT_SEED_ANCHOR_BLOCK_HASH_DIGEST_MISMATCH, ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_EMPTY, ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_VALID_EMPY_SMT, }, @@ -28,26 +29,26 @@ use miden_lib::{ }; use miden_objects::{ accounts::{ - AccountBuilder, AccountComponent, AccountProcedureInfo, AccountStorage, AccountType, - StorageSlot, + Account, AccountBuilder, AccountIdAnchor, AccountProcedureInfo, AccountStorageMode, + AccountType, StorageSlot, }, testing::{ - account_component::BASIC_WALLET_CODE, - constants::FUNGIBLE_FAUCET_INITIAL_BALANCE, + account_component::AccountMockComponent, storage::{generate_account_seed, AccountSeedType}, }, transaction::{TransactionArgs, TransactionScript}, - Digest, FieldElement, + BlockHeader, GENESIS_BLOCK, }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use vm_processor::{AdviceInputs, ONE}; +use vm_processor::{AdviceInputs, Digest, ExecutionError, ONE}; use super::{Felt, Process, Word, ZERO}; use crate::{ assert_execution_error, testing::{ - utils::input_note_data_ptr, MockHost, TransactionContext, TransactionContextBuilder, + utils::input_note_data_ptr, MockChain, MockHost, TransactionContext, + TransactionContextBuilder, }, tests::kernel_tests::read_root_mem_value, }; @@ -114,8 +115,13 @@ fn global_input_memory_assertions(process: &Process, inputs: &Transact assert_eq!( read_root_mem_value(process, ACCT_ID_PTR)[0], - inputs.account().id().into(), - "The account ID should be stored at the ACCT_ID_PTR" + inputs.account().id().second_felt(), + "The account ID first felt should be stored at the ACCT_ID_PTR[0]" + ); + assert_eq!( + read_root_mem_value(process, ACCT_ID_PTR)[1], + inputs.account().id().first_felt(), + "The account ID second felt should be stored at the ACCT_ID_PTR[1]" ); assert_eq!( @@ -240,7 +246,12 @@ fn chain_mmr_memory_assertions(process: &Process, prepared_tx: &Transa fn account_data_memory_assertions(process: &Process, inputs: &TransactionContext) { assert_eq!( read_root_mem_value(process, NATIVE_ACCT_ID_AND_NONCE_PTR), - [inputs.account().id().into(), ZERO, ZERO, inputs.account().nonce()], + [ + inputs.account().id().second_felt(), + inputs.account().id().first_felt(), + ZERO, + inputs.account().nonce() + ], "The account id should be stored at NATIVE_ACCT_ID_AND_NONCE_PTR[0]" ); @@ -392,173 +403,210 @@ fn input_notes_memory_assertions( } } -#[cfg_attr(not(feature = "testing"), ignore)] -#[test] -pub fn test_prologue_create_account() { - let (account, seed) = AccountBuilder::new() - .init_seed(ChaCha20Rng::from_entropy().gen()) - .with_component( - AccountComponent::compile( - BASIC_WALLET_CODE, - TransactionKernel::testing_assembler(), - AccountStorage::mock_storage_slots(), - ) - .unwrap() - .with_supported_type(AccountType::RegularAccountUpdatableCode), - ) - .build() - .unwrap(); +// ACCOUNT CREATION TESTS +// ================================================================================================ - let tx_context = TransactionContextBuilder::new(account).account_seed(Some(seed)).build(); +/// Test helper which executes the prologue to check if the creation of the given `account` with its +/// `seed` is valid in the context of the given `mock_chain`. +pub fn create_account_test( + mock_chain: &MockChain, + account: Account, + seed: Word, +) -> Result<(), ExecutionError> { + let tx_inputs = mock_chain.get_transaction_inputs(account.clone(), Some(seed), &[], &[]); + + let tx_context = TransactionContextBuilder::new(account) + .account_seed(Some(seed)) + .tx_inputs(tx_inputs) + .build(); let code = " - use.kernel::prologue + use.kernel::prologue - begin - exec.prologue::prepare_transaction - end - "; + begin + exec.prologue::prepare_transaction + end + "; - tx_context.execute_code(code).unwrap(); + tx_context.execute_code(code)?; + + Ok(()) } -#[cfg_attr(not(feature = "testing"), ignore)] -#[test] -pub fn test_prologue_create_account_valid_fungible_faucet_reserved_slot() { - let (acct_id, account_seed) = generate_account_seed( - AccountSeedType::FungibleFaucetValidInitialBalance, - TransactionKernel::assembler().with_debug_mode(true), - ); +pub fn create_multiple_accounts_test( + mock_chain: &MockChain, + anchor_block_header: &BlockHeader, + storage_mode: AccountStorageMode, +) -> anyhow::Result<()> { + let mut accounts = Vec::new(); + + for account_type in [ + AccountType::RegularAccountImmutableCode, + AccountType::RegularAccountUpdatableCode, + AccountType::FungibleFaucet, + AccountType::NonFungibleFaucet, + ] { + let (account, seed) = AccountBuilder::new() + .account_type(account_type) + .storage_mode(storage_mode) + .init_seed(ChaCha20Rng::from_entropy().gen()) + .anchor( + AccountIdAnchor::try_from(anchor_block_header) + .context("block header to anchor conversion failed")?, + ) + .with_component( + AccountMockComponent::new_with_slots( + TransactionKernel::testing_assembler(), + vec![StorageSlot::Value([Felt::new(255); 4])], + ) + .unwrap(), + ) + .build() + .context("account build failed")?; - let tx_context = - TransactionContextBuilder::with_fungible_faucet(acct_id.into(), Felt::ZERO, ZERO) - .account_seed(Some(account_seed)) - .build(); + accounts.push((account, seed)); + } - let code = " - use.kernel::prologue + for (account, seed) in accounts { + let account_type = account.account_type(); + create_account_test(mock_chain, account, seed).context(format!( + "create_multiple_accounts_test test failed for account type {:?}", + account_type + ))?; + } - begin - exec.prologue::prepare_transaction - end - "; + Ok(()) +} + +/// Tests that a valid account of each type can be created successfully with the genesis block used +/// as the anchor block for the account IDs. +#[test] +pub fn create_accounts_with_anchor_block_zero() -> anyhow::Result<()> { + let mut mock_chain = MockChain::new(); + // Choose epoch block 0 as the anchor block. + // Here the transaction reference block is also the anchor block. + let genesis_block_header = mock_chain.block_header(GENESIS_BLOCK as usize); + + create_multiple_accounts_test(&mock_chain, &genesis_block_header, AccountStorageMode::Private)?; - tx_context.execute_code(code).unwrap(); + // Seal one more block to test the case where the transaction reference block is not the anchor + // block. + mock_chain.seal_block(None); + + create_multiple_accounts_test(&mock_chain, &genesis_block_header, AccountStorageMode::Public) } -#[cfg_attr(not(feature = "testing"), ignore)] +/// Tests that a valid account of each type can be created successfully with an epoch block whose +/// number is non-zero used as the anchor block for the account IDs. +/// +/// Note that this test is very slow in debug mode. #[test] -pub fn test_prologue_create_account_invalid_fungible_faucet_reserved_slot() { - let (acct_id, account_seed) = generate_account_seed( - AccountSeedType::FungibleFaucetInvalidInitialBalance, - TransactionKernel::assembler(), - ); +pub fn create_accounts_with_non_zero_anchor_block() -> anyhow::Result<()> { + let mut mock_chain = MockChain::new(); + mock_chain.seal_block(Some(1 << 16)); - let tx_context = TransactionContextBuilder::with_fungible_faucet( - acct_id.into(), - Felt::ZERO, - Felt::new(FUNGIBLE_FAUCET_INITIAL_BALANCE), - ) - .account_seed(Some(account_seed)) - .build(); + // Choose epoch block 1 whose block number is 2^16 as the anchor block. + // Here the transaction reference block is also the anchor block. + let epoch1_block_header = mock_chain.block_header(1 << 16); - let code = " - use.kernel::prologue + create_multiple_accounts_test(&mock_chain, &epoch1_block_header, AccountStorageMode::Private)?; - begin - exec.prologue::prepare_transaction - end - "; + // Seal one more block to test the case where the transaction reference block is not the anchor + // block. + mock_chain.seal_block(None); - let process = tx_context.execute_code(code); - assert_execution_error!(process, ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_EMPTY); + create_multiple_accounts_test(&mock_chain, &epoch1_block_header, AccountStorageMode::Public) } -#[cfg_attr(not(feature = "testing"), ignore)] +/// Tests that creating a fungible faucet account with a non-empty initial balance in its reserved +/// slot fails. #[test] -pub fn test_prologue_create_account_valid_non_fungible_faucet_reserved_slot() { - let (acct_id, account_seed) = generate_account_seed( - AccountSeedType::NonFungibleFaucetValidReservedSlot, +pub fn create_account_fungible_faucet_invalid_initial_balance() -> anyhow::Result<()> { + let mut mock_chain = MockChain::new(); + mock_chain.seal_block(None); + + let genesis_block_header = mock_chain.block_header(GENESIS_BLOCK as usize); + + let (account, _, account_seed) = generate_account_seed( + AccountSeedType::FungibleFaucetInvalidInitialBalance, + &genesis_block_header, TransactionKernel::assembler().with_debug_mode(true), ); - let tx_context = - TransactionContextBuilder::with_non_fungible_faucet(acct_id.into(), Felt::ZERO, true) - .account_seed(Some(account_seed)) - .build(); + let result = create_account_test(&mock_chain, account, account_seed); - let code = " - use.kernel::prologue + assert_execution_error!(result, ERR_PROLOGUE_NEW_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_EMPTY); - begin - exec.prologue::prepare_transaction - end - "; - - let process = tx_context.execute_code(code); - - assert!(process.is_ok()) + Ok(()) } -#[cfg_attr(not(feature = "testing"), ignore)] +/// Tests that creating a non fungible faucet account with a non-empty SMT in its reserved slot +/// fails. #[test] -pub fn test_prologue_create_account_invalid_non_fungible_faucet_reserved_slot() { - let (acct_id, account_seed) = generate_account_seed( +pub fn create_account_non_fungible_faucet_invalid_initial_reserved_slot() -> anyhow::Result<()> { + let mut mock_chain = MockChain::new(); + mock_chain.seal_block(None); + + let genesis_block_header = mock_chain.block_header(GENESIS_BLOCK as usize); + + let (account, _, account_seed) = generate_account_seed( AccountSeedType::NonFungibleFaucetInvalidReservedSlot, + &genesis_block_header, TransactionKernel::assembler().with_debug_mode(true), ); - let tx_context = - TransactionContextBuilder::with_non_fungible_faucet(acct_id.into(), Felt::ZERO, false) - .account_seed(Some(account_seed)) - .build(); - - let code = " - use.kernel::prologue - - begin - exec.prologue::prepare_transaction - end - "; - - let process = tx_context.execute_code(code); + let result = create_account_test(&mock_chain, account, account_seed); assert_execution_error!( - process, + result, ERR_PROLOGUE_NEW_NON_FUNGIBLE_FAUCET_RESERVED_SLOT_MUST_BE_VALID_EMPY_SMT ); + + Ok(()) } -#[cfg_attr(not(feature = "testing"), ignore)] +/// Tests that supplying an invalid seed causes account creation to fail. +/// +/// TODO: Add variant of this test with incorrect block hash. #[test] -pub fn test_prologue_create_account_invalid_seed() { - let (acct, account_seed) = AccountBuilder::new() +pub fn create_account_invalid_seed() { + let mut mock_chain = MockChain::new(); + mock_chain.seal_block(None); + + let genesis_block_header = mock_chain.block_header(GENESIS_BLOCK as usize); + + let (account, seed) = AccountBuilder::new() + .anchor(AccountIdAnchor::try_from(&genesis_block_header).unwrap()) .init_seed(ChaCha20Rng::from_entropy().gen()) .account_type(AccountType::RegularAccountUpdatableCode) .with_component(BasicWallet) .build() .unwrap(); - let code = " - use.kernel::prologue - - begin - exec.prologue::prepare_transaction - end - "; + let tx_inputs = mock_chain.get_transaction_inputs(account.clone(), Some(seed), &[], &[]); // override the seed with an invalid seed to ensure the kernel fails - let account_seed_key = [acct.id().into(), ZERO, ZERO, ZERO]; + let account_seed_key = [account.id().second_felt(), account.id().first_felt(), ZERO, ZERO]; let adv_inputs = AdviceInputs::default().with_map([(Digest::from(account_seed_key), vec![ZERO; 4])]); - let tx_context = TransactionContextBuilder::new(acct) - .account_seed(Some(account_seed)) + let tx_context = TransactionContextBuilder::new(account) + .account_seed(Some(seed)) + .tx_inputs(tx_inputs) .advice_inputs(adv_inputs) .build(); - let process = tx_context.execute_code(code); - assert_execution_error!(process, ERR_ACCOUNT_SEED_DIGEST_MISMATCH) + let code = " + use.kernel::prologue + + begin + exec.prologue::prepare_transaction + end + "; + + let result = tx_context.execute_code(code); + + assert_execution_error!(result, ERR_ACCOUNT_SEED_ANCHOR_BLOCK_HASH_DIGEST_MISMATCH) } #[test] diff --git a/miden-tx/src/tests/kernel_tests/test_tx.rs b/miden-tx/src/tests/kernel_tests/test_tx.rs index 17d4a69e7..96b76d6f1 100644 --- a/miden-tx/src/tests/kernel_tests/test_tx.rs +++ b/miden-tx/src/tests/kernel_tests/test_tx.rs @@ -19,21 +19,21 @@ use miden_lib::{ }; use miden_objects::{ accounts::{ - account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, - }, - Account, AccountBuilder, AccountComponent, AccountProcedureInfo, AccountStorage, + Account, AccountBuilder, AccountComponent, AccountId, AccountProcedureInfo, AccountStorage, StorageSlot, }, assets::NonFungibleAsset, crypto::merkle::{LeafIndex, MerklePath}, notes::{ - Note, NoteAssets, NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, NoteTag, - NoteType, + Note, NoteAssets, NoteExecutionHint, NoteExecutionMode, NoteInputs, NoteMetadata, + NoteRecipient, NoteTag, NoteType, }, testing::{ - account_component::AccountMockComponent, constants::NON_FUNGIBLE_ASSET_DATA_2, - prepare_word, storage::STORAGE_LEAVES_2, + account_component::AccountMockComponent, + account_id::{ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2}, + constants::NON_FUNGIBLE_ASSET_DATA_2, + prepare_word, + storage::STORAGE_LEAVES_2, }, transaction::{OutputNote, OutputNotes, TransactionScript}, FieldElement, ACCOUNT_TREE_DEPTH, @@ -57,7 +57,7 @@ fn test_create_note() { let recipient = [ZERO, ONE, Felt::new(2), Felt::new(3)]; let aux = Felt::new(27); - let tag = Felt::new(4); + let tag = NoteTag::from_account_id(account_id, NoteExecutionMode::Local).unwrap(); let code = format!( " @@ -82,7 +82,7 @@ fn test_create_note() { ", recipient = prepare_word(&recipient), PUBLIC_NOTE = NoteType::Public as u8, - note_execution_hint = Felt::from(NoteExecutionHint::after_block(23)), + note_execution_hint = Felt::from(NoteExecutionHint::after_block(23).unwrap()), tag = tag, ); @@ -103,8 +103,8 @@ fn test_create_note() { let expected_note_metadata: Word = NoteMetadata::new( account_id, NoteType::Public, - tag.try_into().unwrap(), - NoteExecutionHint::after_block(23), + tag, + NoteExecutionHint::after_block(23).unwrap(), Felt::new(27), ) .unwrap() @@ -242,7 +242,7 @@ fn test_get_output_notes_commitment() { tx_context.tx_inputs().account().id(), NoteType::Public, output_tag_2, - NoteExecutionHint::after_block(123), + NoteExecutionHint::after_block(123).unwrap(), ZERO, ) .unwrap(); @@ -364,10 +364,11 @@ fn test_get_output_notes_commitment() { fn test_create_note_and_add_asset() { let tx_context = TransactionContextBuilder::with_standard_account(ONE).build(); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); let recipient = [ZERO, ONE, Felt::new(2), Felt::new(3)]; let aux = Felt::new(27); let tag = Felt::new(4); - let asset = [Felt::new(10), ZERO, ZERO, Felt::new(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN)]; + let asset = [Felt::new(10), ZERO, faucet_id.second_felt(), faucet_id.first_felt()]; let code = format!( " @@ -425,14 +426,18 @@ fn test_create_note_and_add_asset() { fn test_create_note_and_add_multiple_assets() { let tx_context = TransactionContextBuilder::with_standard_account(ONE).build(); + let faucet = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + let faucet_2 = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2).unwrap(); + let recipient = [ZERO, ONE, Felt::new(2), Felt::new(3)]; let aux = Felt::new(27); let tag = Felt::new(4); - let asset = [Felt::new(10), ZERO, ZERO, Felt::new(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN)]; - let asset_2 = [Felt::new(20), ZERO, ZERO, Felt::new(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2)]; - let asset_3 = [Felt::new(30), ZERO, ZERO, Felt::new(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2)]; - let asset_2_and_3 = - [Felt::new(50), ZERO, ZERO, Felt::new(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2)]; + + let asset = [Felt::new(10), ZERO, faucet.second_felt(), faucet.first_felt()]; + let asset_2 = [Felt::new(20), ZERO, faucet_2.second_felt(), faucet_2.first_felt()]; + let asset_3 = [Felt::new(30), ZERO, faucet_2.second_felt(), faucet_2.first_felt()]; + let asset_2_and_3 = [Felt::new(50), ZERO, faucet_2.second_felt(), faucet_2.first_felt()]; + let non_fungible_asset = NonFungibleAsset::mock(&NON_FUNGIBLE_ASSET_DATA_2); let non_fungible_asset_encoded = Word::from(non_fungible_asset); @@ -626,7 +631,7 @@ fn test_build_recipient_hash() { output_serial_no = prepare_word(&output_serial_no), PUBLIC_NOTE = NoteType::Public as u8, tag = tag, - execution_hint = Felt::from(NoteExecutionHint::after_block(2)), + execution_hint = Felt::from(NoteExecutionHint::after_block(2).unwrap()), aux = aux, ); @@ -702,7 +707,6 @@ fn test_fpi_memory() { .build_existing() .unwrap(); - let foreign_account_id = foreign_account.id(); let mut mock_chain = MockChain::with_accounts(&[native_account.clone(), foreign_account.clone()]); mock_chain.seal_block(None); @@ -740,8 +744,8 @@ fn test_fpi_memory() { push.{get_item_foreign_hash} # push the foreign account id - push.{foreign_account_id} - # => [foreign_account_id, FOREIGN_PROC_ROOT, storage_item_index, pad(11)] + push.{foreign_second_felt}.{foreign_first_felt} + # => [foreign_account_id_hi, foreign_account_id_lo, FOREIGN_PROC_ROOT, storage_item_index, pad(11)] exec.tx::execute_foreign_procedure # => [STORAGE_VALUE_1] @@ -750,6 +754,8 @@ fn test_fpi_memory() { exec.sys::truncate_stack end ", + foreign_first_felt = foreign_account.id().first_felt(), + foreign_second_felt = foreign_account.id().second_felt(), get_item_foreign_hash = foreign_account.code().procedures()[0].mast_root(), ); @@ -791,8 +797,8 @@ fn test_fpi_memory() { push.{get_map_item_foreign_hash} # push the foreign account id - push.{foreign_account_id} - # => [foreign_account_id, FOREIGN_PROC_ROOT, storage_item_index, MAP_ITEM_KEY, pad(10)] + push.{foreign_second_felt}.{foreign_first_felt} + # => [foreign_account_id_hi, foreign_account_id_lo, FOREIGN_PROC_ROOT, storage_item_index, MAP_ITEM_KEY, pad(10)] exec.tx::execute_foreign_procedure # => [MAP_VALUE] @@ -801,6 +807,8 @@ fn test_fpi_memory() { exec.sys::truncate_stack end ", + foreign_first_felt = foreign_account.id().first_felt(), + foreign_second_felt = foreign_account.id().second_felt(), map_key = STORAGE_LEAVES_2[0].0, get_map_item_foreign_hash = foreign_account.code().procedures()[1].mast_root(), ); @@ -843,8 +851,8 @@ fn test_fpi_memory() { push.{get_item_foreign_hash} # push the foreign account id - push.{foreign_account_id} - # => [foreign_account_id, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] + push.{foreign_second_felt}.{foreign_first_felt} + # => [foreign_account_id_hi, foreign_account_id_lo, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] exec.tx::execute_foreign_procedure dropw # => [] @@ -861,8 +869,8 @@ fn test_fpi_memory() { push.{get_item_foreign_hash} # push the foreign account id - push.{foreign_account_id} - # => [foreign_account_id, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] + push.{foreign_second_felt}.{foreign_first_felt} + # => [foreign_account_id_hi, foreign_account_id_lo, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] exec.tx::execute_foreign_procedure @@ -870,6 +878,8 @@ fn test_fpi_memory() { exec.sys::truncate_stack end ", + foreign_first_felt = foreign_account.id().first_felt(), + foreign_second_felt = foreign_account.id().second_felt(), get_item_foreign_hash = foreign_account.code().procedures()[0].mast_root(), ); @@ -966,8 +976,8 @@ fn test_fpi_execute_foreign_procedure() { push.{get_item_foreign_hash} # push the foreign account id - push.{foreign_account_id} - # => [foreign_account_id, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] + push.{foreign_second_felt}.{foreign_first_felt} + # => [foreign_account_id_hi, foreign_account_id_lo, FOREIGN_PROC_ROOT, storage_item_index, pad(14)] exec.tx::execute_foreign_procedure # => [STORAGE_VALUE] @@ -991,8 +1001,8 @@ fn test_fpi_execute_foreign_procedure() { push.{get_map_item_foreign_hash} # push the foreign account id - push.{foreign_account_id} - # => [foreign_account_id, FOREIGN_PROC_ROOT, storage_item_index, MAP_ITEM_KEY, pad(10)] + push.{foreign_second_felt}.{foreign_first_felt} + # => [foreign_account_id_hi, foreign_account_id_lo, FOREIGN_PROC_ROOT, storage_item_index, MAP_ITEM_KEY, pad(10)] exec.tx::execute_foreign_procedure # => [MAP_VALUE] @@ -1005,7 +1015,8 @@ fn test_fpi_execute_foreign_procedure() { exec.sys::truncate_stack end ", - foreign_account_id = foreign_account.id(), + foreign_first_felt = foreign_account.id().first_felt(), + foreign_second_felt = foreign_account.id().second_felt(), get_item_foreign_hash = foreign_account.code().procedures()[0].mast_root(), get_map_item_foreign_hash = foreign_account.code().procedures()[1].mast_root(), map_key = STORAGE_LEAVES_2[0].0, @@ -1057,12 +1068,13 @@ fn get_mock_fpi_adv_inputs(foreign_account: &Account, mock_chain: &MockChain) -> foreign_account.code(), &foreign_account.storage().get_header(), // Provide the merkle path of the foreign account to be able to verify that the account - // database has the hash of the this foreign account. Verification is done during the + // database has the hash of this foreign account. Verification is done during the // execution of the `kernel::account::validate_current_foreign_account` procedure. &MerklePath::new( mock_chain .accounts() - .open(&LeafIndex::::new(foreign_account.id().into()).unwrap()) + // TODO: Update. + .open(&LeafIndex::::new(foreign_account.id().first_felt().as_int()).unwrap()) .path .into(), ), @@ -1088,7 +1100,12 @@ fn foreign_account_data_memory_assertions(foreign_account: &Account, process: &P assert_eq!( read_root_mem_value(process, foreign_account_data_ptr + ACCT_ID_AND_NONCE_OFFSET), - [foreign_account.id().into(), ZERO, ZERO, foreign_account.nonce()], + [ + foreign_account.id().second_felt(), + foreign_account.id().first_felt(), + ZERO, + foreign_account.nonce() + ], ); assert_eq!( diff --git a/miden-tx/src/tests/mod.rs b/miden-tx/src/tests/mod.rs index fa1c1d86f..a924ebe24 100644 --- a/miden-tx/src/tests/mod.rs +++ b/miden-tx/src/tests/mod.rs @@ -12,10 +12,6 @@ use ::assembly::{ use miden_lib::transaction::TransactionKernel; use miden_objects::{ accounts::{ - account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, - ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, - }, AccountBuilder, AccountCode, AccountComponent, AccountStorage, AccountType, StorageSlot, }, assembly::DefaultSourceManager, @@ -26,6 +22,10 @@ use miden_objects::{ }, testing::{ account_component::AccountMockComponent, + account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, + ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, + }, constants::{FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}, notes::DEFAULT_NOTE_CODE, prepare_word, @@ -446,8 +446,7 @@ fn test_send_note_proc() { .with_mock_notes_preserved_with_account_vault_delta() .build(); - let executor = - TransactionExecutor::new(tx_context.get_data_store(), None).with_debug_mode(true); + let executor = TransactionExecutor::new(tx_context.get_data_store(), None).with_debug_mode(); let account_id = tx_context.tx_inputs().account().id(); // removed assets @@ -479,7 +478,7 @@ fn test_send_note_proc() { vec![removed_asset_1, removed_asset_2, removed_asset_3], ]; - for removed_assets in assets_matrix { + for (idx, removed_assets) in assets_matrix.into_iter().enumerate() { // Prepare the string containing the procedures required for adding assets to the note. // Depending on the number of the assets to remove, the resulting string will be extended // with the corresponding number of procedure "blocks" @@ -563,8 +562,9 @@ fn test_send_note_proc() { // expected delta // -------------------------------------------------------------------------------------------- // execute the transaction and get the witness - let executed_transaction = - executor.execute_transaction(account_id, block_ref, ¬e_ids, tx_args).unwrap(); + let executed_transaction = executor + .execute_transaction(account_id, block_ref, ¬e_ids, tx_args) + .unwrap_or_else(|_| panic!("test failed in iteration {idx}")); // nonce delta // -------------------------------------------------------------------------------------------- @@ -591,8 +591,7 @@ fn executed_transaction_output_notes() { .with_mock_notes_preserved_with_account_vault_delta() .build(); - let executor = - TransactionExecutor::new(tx_context.get_data_store(), None).with_debug_mode(true); + let executor = TransactionExecutor::new(tx_context.get_data_store(), None).with_debug_mode(); let account_id = tx_context.tx_inputs().account().id(); // removed assets @@ -982,14 +981,14 @@ fn transaction_executor_account_code_using_custom_library() { .unwrap() .with_supports_all_types(); - let (native_account, seed) = AccountBuilder::new() + // Build an existing account with nonce 1. + let native_account = AccountBuilder::new() .init_seed(ChaCha20Rng::from_entropy().gen()) .with_component(account_component) - .build() + .build_existing() .unwrap(); - let tx_context = - TransactionContextBuilder::new(native_account).account_seed(Some(seed)).build(); + let tx_context = TransactionContextBuilder::new(native_account).build(); let tx_script = TransactionScript::compile( tx_script_src, @@ -1016,6 +1015,6 @@ fn transaction_executor_account_code_using_custom_library() { let executed_tx = executor.execute_transaction(account_id, block_ref, &[], tx_args).unwrap(); - // Account nonce should have been incremented by 4. - assert_eq!(executed_tx.account_delta().nonce().unwrap(), Felt::new(4)); + // Account's initial nonce of 1 should have been incremented by 4. + assert_eq!(executed_tx.account_delta().nonce().unwrap(), Felt::new(5)); } diff --git a/miden-tx/tests/integration/main.rs b/miden-tx/tests/integration/main.rs index a4fff10b6..3ed0845fd 100644 --- a/miden-tx/tests/integration/main.rs +++ b/miden-tx/tests/integration/main.rs @@ -5,10 +5,11 @@ mod wallet; use miden_lib::transaction::TransactionKernel; use miden_objects::{ - accounts::{account_id::testing::ACCOUNT_ID_SENDER, AccountId}, + accounts::AccountId, assets::FungibleAsset, crypto::utils::Serializable, notes::{Note, NoteAssets, NoteInputs, NoteMetadata, NoteRecipient, NoteScript, NoteType}, + testing::account_id::ACCOUNT_ID_SENDER, transaction::{ExecutedTransaction, ProvenTransaction}, Felt, Word, ZERO, }; diff --git a/miden-tx/tests/integration/scripts/p2id.rs b/miden-tx/tests/integration/scripts/p2id.rs index d92f9757f..1a83f4918 100644 --- a/miden-tx/tests/integration/scripts/p2id.rs +++ b/miden-tx/tests/integration/scripts/p2id.rs @@ -3,18 +3,18 @@ use miden_lib::{ transaction::TransactionKernel, }; use miden_objects::{ - accounts::{ - account_id::testing::{ + accounts::Account, + assets::{Asset, AssetVault, FungibleAsset}, + crypto::rand::RpoRandomCoin, + notes::NoteType, + testing::{ + account_id::{ ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN_2, ACCOUNT_ID_SENDER, }, - Account, + prepare_word, }, - assets::{Asset, AssetVault, FungibleAsset}, - crypto::rand::RpoRandomCoin, - notes::NoteType, - testing::prepare_word, transaction::{OutputNote, TransactionScript}, Felt, }; @@ -191,14 +191,10 @@ fn test_create_consume_multiple_notes() { let mut account = mock_chain.add_existing_wallet(Auth::BasicAuth, vec![FungibleAsset::mock(20)]); - let input_note_faucet_id = ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN; - let input_note_asset_1: Asset = - FungibleAsset::new(input_note_faucet_id.try_into().unwrap(), 11).unwrap().into(); + let input_note_faucet_id = ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN.try_into().unwrap(); + let input_note_asset_1: Asset = FungibleAsset::new(input_note_faucet_id, 11).unwrap().into(); - let input_note_asset_2: Asset = - FungibleAsset::new(input_note_faucet_id.try_into().unwrap(), 100) - .unwrap() - .into(); + let input_note_asset_2: Asset = FungibleAsset::new(input_note_faucet_id, 100).unwrap().into(); let input_note_1 = mock_chain .add_p2id_note( @@ -296,11 +292,7 @@ fn test_create_consume_multiple_notes() { assert_eq!(executed_transaction.output_notes().num_notes(), 2); account.apply_delta(executed_transaction.account_delta()).unwrap(); - for asset in account.vault().assets() { - if u64::from(asset.faucet_id()) == input_note_faucet_id { - assert!(asset.unwrap_fungible().amount() == 111); - } else if asset.faucet_id() == FungibleAsset::mock_issuer() { - assert!(asset.unwrap_fungible().amount() == 5); - } - } + + assert_eq!(account.vault().get_balance(input_note_faucet_id).unwrap(), 111); + assert_eq!(account.vault().get_balance(FungibleAsset::mock_issuer()).unwrap(), 5); } diff --git a/miden-tx/tests/integration/wallet/mod.rs b/miden-tx/tests/integration/wallet/mod.rs index 56f41a85b..a2ef89e54 100644 --- a/miden-tx/tests/integration/wallet/mod.rs +++ b/miden-tx/tests/integration/wallet/mod.rs @@ -6,7 +6,10 @@ use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; #[test] fn wallet_creation() { use miden_lib::accounts::{auth::RpoFalcon512, wallets::BasicWallet}; - use miden_objects::accounts::{AccountCode, AccountStorageMode, AccountType}; + use miden_objects::{ + accounts::{AccountCode, AccountStorageMode, AccountType}, + digest, BlockHeader, + }; // we need a Falcon Public Key to create the wallet account let seed = [0_u8; 32]; @@ -25,8 +28,17 @@ fn wallet_creation() { let account_type = AccountType::RegularAccountImmutableCode; let storage_mode = AccountStorageMode::Private; - let (wallet, _) = - create_basic_wallet(init_seed, auth_scheme, account_type, storage_mode).unwrap(); + let anchor_block_header_mock = + BlockHeader::mock(0, Some(digest!("0xaa")), Some(digest!("0xbb")), &[], digest!("0xcc")); + + let (wallet, _) = create_basic_wallet( + init_seed, + (&anchor_block_header_mock).try_into().unwrap(), + auth_scheme, + account_type, + storage_mode, + ) + .unwrap(); let expected_code = AccountCode::from_components( &[RpoFalcon512::new(pub_key).into(), BasicWallet.into()], diff --git a/objects/Cargo.toml b/objects/Cargo.toml index 779559ee5..9681904c8 100644 --- a/objects/Cargo.toml +++ b/objects/Cargo.toml @@ -40,6 +40,7 @@ winter-rand-utils = { version = "0.10", optional = true } getrandom = { version = "0.2", features = ["js"] } [dev-dependencies] +anyhow = { version = "1.0.93", default-features = false, features = ["std", "backtrace"]} assert_matches = { workspace = true } criterion = { version = "0.5", default-features = false, features = ["html_reports"] } miden-objects = { path = ".", features = ["testing"] } diff --git a/objects/benches/account_seed.rs b/objects/benches/account_seed.rs index 426db03cd..3c04d9079 100644 --- a/objects/benches/account_seed.rs +++ b/objects/benches/account_seed.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; use miden_objects::{ - accounts::{AccountId, AccountStorageMode, AccountType}, + accounts::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}, Digest, }; @@ -12,10 +12,12 @@ fn grind_account_seed(c: &mut Criterion) { c.bench_function("Grind regular on-chain account seed", |bench| { bench.iter(|| { - AccountId::get_account_seed( + AccountId::compute_account_seed( init_seed, AccountType::RegularAccountImmutableCode, AccountStorageMode::Public, + AccountIdVersion::VERSION_0, + Digest::default(), Digest::default(), Digest::default(), ) @@ -24,10 +26,12 @@ fn grind_account_seed(c: &mut Criterion) { c.bench_function("Grind fungible faucet on-chain account seed", |bench| { bench.iter(|| { - AccountId::get_account_seed( + AccountId::compute_account_seed( init_seed, AccountType::FungibleFaucet, AccountStorageMode::Public, + AccountIdVersion::VERSION_0, + Digest::default(), Digest::default(), Digest::default(), ) diff --git a/objects/src/accounts/account_id.rs b/objects/src/accounts/account_id.rs index badee6795..d3fe3de97 100644 --- a/objects/src/accounts/account_id.rs +++ b/objects/src/accounts/account_id.rs @@ -4,31 +4,26 @@ use alloc::{ }; use core::{fmt, str::FromStr}; -use super::{ - get_account_seed, AccountError, ByteReader, Deserializable, DeserializationError, Digest, Felt, - Hasher, Serializable, Word, ZERO, +use miden_crypto::{merkle::LeafIndex, utils::hex_to_bytes}; +use vm_core::{ + utils::{ByteReader, Deserializable, Serializable}, + Felt, Word, }; -use crate::{crypto::merkle::LeafIndex, utils::hex_to_bytes, ACCOUNT_TREE_DEPTH}; +use vm_processor::{DeserializationError, Digest}; -// CONSTANTS -// ================================================================================================ - -// The higher two bits of the most significant nibble determines the account storage mode -pub const ACCOUNT_STORAGE_MASK_SHIFT: u64 = 62; -pub const ACCOUNT_STORAGE_MASK: u64 = 0b11 << ACCOUNT_STORAGE_MASK_SHIFT; - -// The lower two bits of the most significant nibble determines the account type -pub const ACCOUNT_TYPE_MASK_SHIFT: u64 = 60; -pub const ACCOUNT_TYPE_MASK: u64 = 0b11 << ACCOUNT_TYPE_MASK_SHIFT; -pub const ACCOUNT_ISFAUCET_MASK: u64 = 0b10 << ACCOUNT_TYPE_MASK_SHIFT; +use super::Hasher; +use crate::{ + accounts::{AccountIdAnchor, AccountIdPrefix}, + AccountError, ACCOUNT_TREE_DEPTH, +}; -// ACCOUNT TYPES +// ACCOUNT TYPE // ================================================================================================ -pub const FUNGIBLE_FAUCET: u64 = 0b10; -pub const NON_FUNGIBLE_FAUCET: u64 = 0b11; -pub const REGULAR_ACCOUNT_IMMUTABLE_CODE: u64 = 0b00; -pub const REGULAR_ACCOUNT_UPDATABLE_CODE: u64 = 0b01; +const FUNGIBLE_FAUCET: u64 = 0b10; +const NON_FUNGIBLE_FAUCET: u64 = 0b11; +const REGULAR_ACCOUNT_IMMUTABLE_CODE: u64 = 0b00; +const REGULAR_ACCOUNT_UPDATABLE_CODE: u64 = 0b01; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[repr(u64)] @@ -51,44 +46,23 @@ impl AccountType { } } -/// Extracts the [AccountType] encoded in an u64. -/// -/// The account id is encoded in the bits `[62,60]` of the u64, see [ACCOUNT_TYPE_MASK]. -/// -/// # Note -/// -/// This function does not validate the u64, it is assumed the value is valid [Felt]. -pub const fn account_type_from_u64(value: u64) -> AccountType { - debug_assert!( - ACCOUNT_TYPE_MASK.count_ones() == 2, - "This method assumes there are only 2bits in the mask" - ); - - let bits = (value & ACCOUNT_TYPE_MASK) >> ACCOUNT_TYPE_MASK_SHIFT; - match bits { - REGULAR_ACCOUNT_UPDATABLE_CODE => AccountType::RegularAccountUpdatableCode, - REGULAR_ACCOUNT_IMMUTABLE_CODE => AccountType::RegularAccountImmutableCode, - FUNGIBLE_FAUCET => AccountType::FungibleFaucet, - NON_FUNGIBLE_FAUCET => AccountType::NonFungibleFaucet, - _ => { - // "account_type mask contains only 2bits, there are 4 options total" - unreachable!() - }, +impl From for AccountType { + fn from(id: AccountId) -> Self { + id.account_type() } } -/// Returns the [AccountType] given an integer representation of `account_id`. -impl From for AccountType { - fn from(value: u64) -> Self { - account_type_from_u64(value) +impl From for AccountType { + fn from(id_prefix: AccountIdPrefix) -> Self { + id_prefix.account_type() } } -// ACCOUNT STORAGE TYPES +// ACCOUNT STORAGE MODE // ================================================================================================ -pub const PUBLIC: u64 = 0b00; -pub const PRIVATE: u64 = 0b10; +const PUBLIC: u64 = 0b00; +const PRIVATE: u64 = 0b10; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u64)] @@ -134,132 +108,284 @@ impl FromStr for AccountStorageMode { } } +impl From for AccountStorageMode { + fn from(id: AccountId) -> Self { + id.storage_mode() + } +} + +impl From for AccountStorageMode { + fn from(id_prefix: AccountIdPrefix) -> Self { + id_prefix.storage_mode() + } +} + +// ACCOUNT ID VERSION +// ================================================================================================ + +/// The version of an [`AccountId`]. +/// +/// Each version has a public associated constant, e.g. [`AccountIdVersion::VERSION_0`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AccountIdVersion(u8); + +impl AccountIdVersion { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + const VERSION_0_NUMBER: u8 = 0; + /// Version 0 of the [`AccountId`]. + pub const VERSION_0: AccountIdVersion = AccountIdVersion(Self::VERSION_0_NUMBER); + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the version number. + pub const fn as_u8(&self) -> u8 { + self.0 + } +} + +impl From for u8 { + fn from(value: AccountIdVersion) -> Self { + value.as_u8() + } +} + +impl From for AccountIdVersion { + fn from(id: AccountId) -> Self { + id.version() + } +} + +impl From for AccountIdVersion { + fn from(id_prefix: AccountIdPrefix) -> Self { + id_prefix.version() + } +} + // ACCOUNT ID // ================================================================================================ -/// Unique identifier of an account. +/// The identifier of an [`Account`](crate::accounts::Account). +/// +/// # Layout +/// +/// An `AccountId` consists of two field elements and is layed out as follows: +/// +/// ```text +/// 1st felt: [random (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] +/// 2nd felt: [anchor_epoch (16 bits) | random (40 bits) | 8 zero bits] +/// ``` +/// +/// # Generation +/// +/// An `AccountId` is a commitment to a user-generated seed, the code and storage of an account and +/// to a certain hash of an epoch block of the blockchain. An id is generated by picking an epoch +/// block as an anchor - which is why it is also referred to as the anchor block - and creating the +/// account's initial storage and code. Then a random seed is picked and the hash of (SEED, +/// CODE_COMMITMENT, STORAGE_COMMITMENT, ANCHOR_BLOCK_HASH) is computed. If the hash's first element +/// has the desired storage mode, account type, version and the high bit set to zero, the +/// computation part of the ID generation is done. If not, another random seed is picked and the +/// process is repeated. The first felt of the ID is then the first element of the hash. +/// +/// The second felt of the ID is the second element of the hash. Its upper 16 bits are overwritten +/// with the epoch in which the ID is anchored and the lower 8 bits are zeroed. Thus, the first felt +/// of the ID must derive exactly from the hash, while only part of the second felt is derived from +/// the hash. +/// +/// # Constraints +/// +/// Constructors will return an error if: +/// +/// - The first felt contains account ID metadata (storage mode, type or version) that does not +/// match any of the known values. +/// - The anchor epoch in the second felt is equal to [`u16::MAX`]. +/// - The lower 8 bits of the second felt are not zero, although [`AccountId::new`] ensures this is +/// the case rather than return an error. /// -/// Account ID consists of 1 field element (~64 bits). The most significant bits in the id are used -/// to encode the account' storage and type. +/// # Design Rationale /// -/// The top two bits are used to encode the storage type. The values [PRIVATE] and [PUBLIC] -/// encode the account's storage type. The next two bits encode the account type. The values -/// [FUNGIBLE_FAUCET], [NON_FUNGIBLE_FAUCET], [REGULAR_ACCOUNT_IMMUTABLE_CODE], and -/// [REGULAR_ACCOUNT_UPDATABLE_CODE] encode the account's type. +/// The rationale behind the above layout is as follows. +/// +/// - The first felt is the output of a hash function so it will be a valid field element without +/// requiring additional constraints. +/// - The version is placed at a static offset such that future ID versions which may change the +/// number of type or storage mode bits will not cause the version to be at a different offset. +/// This is important so that a parser can always reliably read the version and then parse the +/// remainder of the ID depending on the version. Having only 4 bits for the version is a trade +/// off between future proofing to be able to introduce more versions and the version requiring +/// Proof of Work as part of the ID generation. +/// - The version, type and storage mode are part of the first felt which is included in the +/// representation of a non-fungible asset. The first felt alone is enough to determine all of +/// these properties about the ID. +/// - The anchor epoch is not important beyond the creation process, so placing it in the second +/// felt is fine. Moreover, all properties of the first felt must be derived from the seed, so +/// they add to the proof of work difficulty. Adding 16 bits of PoW for the epoch would be +/// significant. +/// - The anchor epoch is placed at the most significant end of the second felt. Its value must be +/// less than [`u16::MAX`] so that at least one of the upper 16 bits is always zero. This ensures +/// that the entire second felt is valid even if the remaining bits of the felt are one. +/// - The lower 8 bits of the second felt may be overwritten when the ID is encoded in other layouts +/// such as the [`NoteMetadata`](crate::notes::NoteMetadata). In such cases, it can happen that +/// all bits of the encoded second felt would be one, so having the epoch constraint is important. +/// - The ID is dependent on the hash of an epoch block. This is a block whose number is a multiple +/// of 2^[`BlockHeader::EPOCH_LENGTH_EXPONENT`][epoch_len_exp], e.g. `0`, `65536`, `131072`, ... +/// These are the first blocks of epoch 0, 1, 2, ... We call this dependence _anchoring_ because +/// the ID is anchored to that epoch block's hash. Anchoring makes it practically impossible for +/// an attacker to construct a rainbow table of account IDs whose epoch is X, if the block for +/// epoch X has not been constructed yet because its hash is then unknown. Therefore, picking a +/// recent anchor block when generating a new ID makes it extremely unlikely that an attacker can +/// highjack this ID because the hash of that block has only been known for a short period of +/// time. +/// - An ID highjack refers to an attack where a user generates an ID and lets someone else send +/// assets to it. At this point the user has not registered the ID on-chain yet, likely +/// because they need the funds in the asset to pay for their first transaction where the +/// account would be registered. Until the ID is registered on chain, an attacker with a +/// rainbow table who happens to have a seed, code and storage commitment combination that +/// hashes to the user's ID can claim the assets sent to the user's ID. Adding the anchor +/// block hash to ID generation process makes this attack practically impossible. +/// +/// [epoch_len_exp]: crate::block::BlockHeader::EPOCH_LENGTH_EXPONENT #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct AccountId(Felt); +pub struct AccountId { + first_felt: Felt, + second_felt: Felt, +} impl AccountId { - /// Specifies a minimum number of trailing zeros required in the last element of the seed - /// digest. - /// - /// Note: The account id includes 4 bits of metadata, these bits determine the account type - /// (normal account, fungible token, non-fungible token), the storage type (on/off chain), and - /// for the normal accounts if the code is updatable or not. These metadata bits are also - /// checked by the PoW and add to the total work defined below. - #[cfg(not(any(feature = "testing", test)))] - pub const REGULAR_ACCOUNT_SEED_DIGEST_MIN_TRAILING_ZEROS: u32 = 23; - #[cfg(not(any(feature = "testing", test)))] - pub const FAUCET_SEED_DIGEST_MIN_TRAILING_ZEROS: u32 = 31; - #[cfg(any(feature = "testing", test))] - pub const REGULAR_ACCOUNT_SEED_DIGEST_MIN_TRAILING_ZEROS: u32 = 5; - #[cfg(any(feature = "testing", test))] - pub const FAUCET_SEED_DIGEST_MIN_TRAILING_ZEROS: u32 = 6; + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The serialized size of an [`AccountId`] in bytes. + pub const SERIALIZED_SIZE: usize = 15; - /// Specifies a minimum number of ones for a valid account ID. - pub const MIN_ACCOUNT_ONES: u32 = 5; + /// The lower two bits of the second least significant nibble determine the account type. + pub(crate) const TYPE_SHIFT: u64 = 4; + pub(crate) const TYPE_MASK: u64 = 0b11 << Self::TYPE_SHIFT; + + /// The least significant nibble determines the account version. + const VERSION_MASK: u64 = 0b1111; + + const ANCHOR_EPOCH_SHIFT: u64 = 48; + const ANCHOR_EPOCH_MASK: u64 = 0xffff << Self::ANCHOR_EPOCH_SHIFT; + + /// The higher two bits of the second least significant nibble determine the account storage + /// mode. + pub(crate) const STORAGE_MODE_SHIFT: u64 = 6; + pub(crate) const STORAGE_MODE_MASK: u64 = 0b11 << Self::STORAGE_MODE_SHIFT; + + pub(crate) const IS_FAUCET_MASK: u64 = 0b10 << Self::TYPE_SHIFT; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Returns a new account ID derived from the specified seed, code commitment and storage - /// commitment. + /// Creates an [`AccountId`] by hashing the given `seed`, `code_commitment`, + /// `storage_commitment` and [`AccountIdAnchor::block_hash`] from the `anchor` and using the + /// resulting first and second element of the hash as the first and second felt of the ID. + /// The [`AccountIdAnchor::epoch`] from the `anchor` overwrites part of the second felt. /// - /// The account ID is computed by hashing the seed, code commitment and storage commitment and - /// using 1 element of the resulting digest to form the ID. Specifically we take element 0. - /// We also require that the last element of the seed digest has at least `23` trailing - /// zeros if it is a regular account, or `31` trailing zeros if it is a faucet account. + /// Note that the `anchor` must correspond to a valid block in the chain for the ID to be deemed + /// valid during creation. /// - /// The seed digest is computed using a sequential hash over - /// hash(SEED, CODE_COMMITMENT, STORAGE_COMMITMENT, ZERO). This takes two permutations. + /// See the documentation of the [`AccountId`] for more details on the generation. /// /// # Errors - /// Returns an error if the resulting account ID does not comply with account ID rules: - /// - the metadata embedded in the ID (i.e., the first 4 bits) is valid. - /// - the ID has at least `5` ones. - /// - the last element of the seed digest has at least `23` trailing zeros for regular accounts. - /// - the last element of the seed digest has at least `31` trailing zeros for faucet accounts. + /// + /// Returns an error if any of the ID constraints are not met. See the [type + /// documentation](AccountId) for details. pub fn new( seed: Word, + anchor: AccountIdAnchor, code_commitment: Digest, storage_commitment: Digest, ) -> Result { - let seed_digest = compute_digest(seed, code_commitment, storage_commitment); + let seed_digest = + compute_digest(seed, code_commitment, storage_commitment, anchor.block_hash()); - Self::validate_seed_digest(&seed_digest)?; - seed_digest[0].try_into() - } + let mut felts: [Felt; 2] = seed_digest.as_elements()[0..2] + .try_into() + .expect("we should have sliced off 2 elements"); - /// Creates a new [AccountId] without checking its validity. - /// - /// This function requires that the provided value is a valid [Felt] representation of an - /// [AccountId]. - pub fn new_unchecked(value: Felt) -> Self { - Self(value) - } + felts[1] = shape_second_felt(felts[1], anchor.epoch()); - /// Creates a new dummy [AccountId] for testing purposes. - #[cfg(any(feature = "testing", test))] - pub fn new_dummy(init_seed: [u8; 32], account_type: AccountType) -> Self { - let code_commitment = Digest::default(); - let storage_commitment = Digest::default(); + // This will validate that the anchor_epoch we have just written is not u16::MAX. + account_id_from_felts(felts) + } - let seed = get_account_seed( - init_seed, - account_type, - AccountStorageMode::Public, - code_commitment, - storage_commitment, - ) - .unwrap(); + /// Creates an [`AccountId`] from the given felts where the felt at index 0 is the first felt + /// and the felt at index 2 is the second felt. + /// + /// # Warning + /// + /// Validity of the ID must be ensured by the caller. An invalid ID may lead to panics. + /// + /// # Panics + /// + /// If debug_assertions are enabled (e.g. in debug mode), this function panics if any of the ID + /// constraints are not met. See the [type documentation](AccountId) for details. + pub fn new_unchecked(elements: [Felt; 2]) -> Self { + let first_felt = elements[0]; + let second_felt = elements[1]; + + // Panic on invalid felts in debug mode. + if cfg!(debug_assertions) { + validate_first_felt(first_felt) + .expect("AccountId::new_unchecked called with invalid first felt"); + validate_second_felt(second_felt) + .expect("AccountId::new_unchecked called with invalid first felt"); + } - Self::new(seed, code_commitment, storage_commitment).unwrap() + Self { first_felt, second_felt } } /// Constructs an [`AccountId`] for testing purposes with the given account type and storage /// mode. /// /// This function does the following: - /// - The bit representation of the account type and storage mode is prepended to the most - /// significant byte of `bytes`. - /// - The 5th most significant bit is cleared. - /// - The lowest 5 bits are set to ensure the id has at least [`Self::MIN_ACCOUNT_ONES`]. - /// - The bytes are then converted to a `u64` in big-endian format. Due to clearing the 5th most - /// significant bit, the resulting `u64` will be a valid [`Felt`]. + /// - Split the given bytes into a `first_felt = bytes[0..8]` and `second_felt = bytes[8..]` + /// part to be used for the first and second felt, respectively. + /// - The least significant byte of the first felt is set to the version 0, and the given type + /// and storage mode. + /// - The 32nd most significant bit in the first felt is cleared to ensure it is a valid felt. + /// The 32nd is chosen as it is the lowest bit that we can clear and still ensure felt + /// validity. This leaves the upper 31 bits to be set by the input `bytes` which makes it + /// simpler to create test values which more often need specific values for the most + /// significant end of the ID. + /// - In the second felt the anchor epoch is set to 0 and the lower 8 bits are cleared. #[cfg(any(feature = "testing", test))] - pub fn new_with_type_and_mode( - mut bytes: [u8; 8], + pub fn new_dummy( + mut bytes: [u8; 15], account_type: AccountType, storage_mode: AccountStorageMode, ) -> AccountId { - let id_high_nibble = (storage_mode as u8) << 6 | (account_type as u8) << 4; - - // Clear the highest five bits of the most significant byte. - // The high nibble must be cleared so we can set it to the storage mode and account type - // we've constructed. - // The 5th most significant bit is cleared to ensure the resulting id is a valid Felt even - // when all other bits are set. - bytes[0] &= 0x07; - // Set high nibble of the most significant byte. - bytes[0] |= id_high_nibble; - // Set the lowest 5 bits to ensure we have at least MIN_ACCOUNT_ONES. - bytes[7] |= 0x1f; - - let account_id = account_id_from_felt( - Felt::try_from(u64::from_be_bytes(bytes)) - .expect("must be a valid felt after clearing the 5th highest bit"), - ) - .expect("account id shoult satisfy criteria of a valid ID"); + let version = AccountIdVersion::VERSION_0_NUMBER; + let low_nibble = (storage_mode as u8) << Self::STORAGE_MODE_SHIFT + | (account_type as u8) << Self::TYPE_SHIFT + | version; + + // Set least significant byte. + bytes[7] = low_nibble; + + // Clear the 32nd most significant bit. + bytes[3] &= 0b1111_1110; + + let first_felt_bytes = + bytes[0..8].try_into().expect("we should have sliced off exactly 8 bytes"); + let first_felt = Felt::try_from(u64::from_be_bytes(first_felt_bytes)) + .expect("should be a valid felt due to the most significant bit being zero"); + + let mut second_felt_bytes = [0; 8]; + // Overwrite first 7 bytes, leaving the 8th byte 0 (which will be cleared by + // shape_second_felt anyway). + second_felt_bytes[..7].copy_from_slice(&bytes[8..]); + // If the value is too large modular reduction is performed, which is fine here. + let mut second_felt = Felt::new(u64::from_be_bytes(second_felt_bytes)); + + second_felt = shape_second_felt(second_felt, 0); + + let account_id = account_id_from_felts([first_felt, second_felt]) + .expect("we should have shaped the felts to produce a valid id"); debug_assert_eq!(account_id.account_type(), account_type); debug_assert_eq!(account_id.storage_mode(), storage_mode); @@ -267,35 +393,54 @@ impl AccountId { account_id } + /// Grinds an account seed until its hash matches the given `account_type`, `storage_mode` and + /// `version` and returns it as a [`Word`]. The input to the hash function next to the seed are + /// the `code_commitment`, `storage_commitment` and `anchor_block_hash`. + /// + /// The grinding process is started from the given `init_seed` which should be a random seed + /// generated from a cryptographically secure source. + pub fn compute_account_seed( + init_seed: [u8; 32], + account_type: AccountType, + storage_mode: AccountStorageMode, + version: AccountIdVersion, + code_commitment: Digest, + storage_commitment: Digest, + anchor_block_hash: Digest, + ) -> Result { + crate::accounts::seed::compute_account_seed( + init_seed, + account_type, + storage_mode, + version, + code_commitment, + storage_commitment, + anchor_block_hash, + ) + } + // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- /// Returns the type of this account ID. pub const fn account_type(&self) -> AccountType { - account_type_from_u64(self.0.as_int()) + extract_type(self.first_felt().as_int()) } - /// Returns true if an account with this ID is a faucet (can issue assets). + /// Returns true if an account with this ID is a faucet which can issue assets. pub fn is_faucet(&self) -> bool { - matches!( - self.account_type(), - AccountType::FungibleFaucet | AccountType::NonFungibleFaucet - ) + self.account_type().is_faucet() } /// Returns true if an account with this ID is a regular account. pub fn is_regular_account(&self) -> bool { - is_regular_account(self.0.as_int()) + self.account_type().is_regular_account() } - /// Returns the storage mode of this account (e.g., public or private). + /// Returns the storage mode of this account ID. pub fn storage_mode(&self) -> AccountStorageMode { - let bits = (self.0.as_int() & ACCOUNT_STORAGE_MASK) >> ACCOUNT_STORAGE_MASK_SHIFT; - match bits { - PUBLIC => AccountStorageMode::Public, - PRIVATE => AccountStorageMode::Private, - _ => panic!("Account with invalid storage bits created"), - } + extract_storage_mode(self.first_felt().as_int()) + .expect("account id should have been constructed with a valid storage mode") } /// Returns true if an account with this ID is a public account. @@ -303,167 +448,171 @@ impl AccountId { self.storage_mode() == AccountStorageMode::Public } - /// Finds and returns a seed suitable for creating an account ID for the specified account type - /// using the provided initial seed as a starting point. - pub fn get_account_seed( - init_seed: [u8; 32], - account_type: AccountType, - storage_mode: AccountStorageMode, - code_commitment: Digest, - storage_commitment: Digest, - ) -> Result { - get_account_seed(init_seed, account_type, storage_mode, code_commitment, storage_commitment) + /// Returns the version of this account ID. + pub fn version(&self) -> AccountIdVersion { + extract_version(self.first_felt().as_int()) + .expect("account id should have been constructed with a valid version") } - /// Creates an Account Id from a hex string. Assumes the string starts with "0x" and + /// Returns the anchor epoch of this account ID. + /// + /// This is the epoch to which this ID is anchored. The hash of this epoch block is used in the + /// generation of the ID. + pub fn anchor_epoch(&self) -> u16 { + extract_anchor_epoch(self.second_felt().as_int()) + } + + /// Creates an [`AccountId`] from a hex string. Assumes the string starts with "0x" and /// that the hexadecimal characters are big-endian encoded. - pub fn from_hex(hex_value: &str) -> Result { - hex_to_bytes(hex_value).map_err(AccountError::AccountIdHexParseError).and_then( - |mut bytes: [u8; 8]| { - // `bytes` ends up being parsed as felt, and the input to that is assumed to be - // little-endian so we need to reverse the order - bytes.reverse(); - bytes.try_into() + pub fn from_hex(hex_str: &str) -> Result { + hex_to_bytes(hex_str).map_err(AccountError::AccountIdHexParseError).and_then( + |mut bytes: [u8; 15]| { + // TryFrom<[u8; 15]> expects [first_felt, second_felt] in little-endian order, so we + // need to convert the bytes representation from big endian to little endian by + // reversing each felt. The first felt has 8 and the second felt has + // 7 bytes. + bytes[0..8].reverse(); + bytes[8..15].reverse(); + + AccountId::try_from(bytes) }, ) } - /// Returns a big-endian, hex-encoded string. + /// Returns a big-endian, hex-encoded string of length 32, including the `0x` prefix, so it + /// encodes 15 bytes. pub fn to_hex(&self) -> String { - format!("0x{:016x}", self.0.as_int()) + // We need to pad the second felt with 16 zeroes so it produces a correctly padded 8 byte + // big-endian hex string. Only then can we cut off the last zero byte by truncating. We + // cannot use `:014x` padding. + let mut hex_string = + format!("0x{:016x}{:016x}", self.first_felt().as_int(), self.second_felt().as_int()); + hex_string.truncate(32); + hex_string } - // UTILITY METHODS - // -------------------------------------------------------------------------------------------- - - /// Returns an error if: - /// - There are fewer then: - /// - 24 trailing ZEROs in the last element of the seed digest for regular accounts. - /// - 32 trailing ZEROs in the last element of the seed digest for faucet accounts. - pub(super) fn validate_seed_digest(digest: &Digest) -> Result<(), AccountError> { - // check the id satisfies the proof-of-work requirement. - let required_zeros = if is_regular_account(digest[0].as_int()) { - Self::REGULAR_ACCOUNT_SEED_DIGEST_MIN_TRAILING_ZEROS - } else { - Self::FAUCET_SEED_DIGEST_MIN_TRAILING_ZEROS - }; - - let trailing_zeros = digest_pow(*digest); - if required_zeros > trailing_zeros { - return Err(AccountError::SeedDigestTooFewTrailingZeros { - expected: required_zeros, - actual: trailing_zeros, - }); - } - - Ok(()) + /// Returns the [`AccountIdPrefix`] of this ID which is equivalent to the first felt. + pub fn prefix(&self) -> AccountIdPrefix { + // SAFETY: We only construct accounts with valid first felts, so we don't have to validate + // it again. + AccountIdPrefix::new_unchecked(self.first_felt) } -} -impl PartialOrd for AccountId { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + /// Returns the first felt of this ID. + pub const fn first_felt(&self) -> Felt { + self.first_felt } -} -impl Ord for AccountId { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.0.as_int().cmp(&other.0.as_int()) - } -} - -impl fmt::Display for AccountId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "0x{:016x}", self.0.as_int()) + /// Returns the second felt of this ID. + pub const fn second_felt(&self) -> Felt { + self.second_felt } } // CONVERSIONS FROM ACCOUNT ID // ================================================================================================ -impl From for Felt { +impl From for [Felt; 2] { fn from(id: AccountId) -> Self { - id.0 + [id.first_felt, id.second_felt] } } -impl From for [u8; 8] { +impl From for [u8; 15] { fn from(id: AccountId) -> Self { - let mut result = [0_u8; 8]; - result[..8].copy_from_slice(&id.0.as_int().to_le_bytes()); + let mut result = [0_u8; 15]; + result[..8].copy_from_slice(&id.first_felt().as_int().to_le_bytes()); + // The last byte of the second felt is always zero, and in little endian this is the first + // byte, so we skip it here. + result[8..].copy_from_slice(&id.second_felt().as_int().to_le_bytes()[1..8]); result } } -impl From for u64 { +impl From for u128 { fn from(id: AccountId) -> Self { - id.0.as_int() + let mut le_bytes = [0_u8; 16]; + le_bytes[..8].copy_from_slice(&id.second_felt().as_int().to_le_bytes()); + le_bytes[8..].copy_from_slice(&id.first_felt().as_int().to_le_bytes()); + u128::from_le_bytes(le_bytes) } } /// Account IDs are used as indexes in the account database, which is a tree of depth 64. impl From for LeafIndex { fn from(id: AccountId) -> Self { - LeafIndex::new_max_depth(id.0.as_int()) + LeafIndex::new_max_depth(id.first_felt().as_int()) } } // CONVERSIONS TO ACCOUNT ID // ================================================================================================ -/// Returns an [AccountId] instantiated with the provided field element. -/// -/// # Errors -/// Returns an error if: -/// - If there are fewer than [AccountId::MIN_ACCOUNT_ONES] in the provided value. -/// - If the provided value contains invalid account ID metadata (i.e., the first 4 bits). -pub fn account_id_from_felt(value: Felt) -> Result { - let int_value = value.as_int(); +impl TryFrom<[Felt; 2]> for AccountId { + type Error = AccountError; - let count = int_value.count_ones(); - if count < AccountId::MIN_ACCOUNT_ONES { - return Err(AccountError::AccountIdTooFewOnes(count)); + /// Returns an [`AccountId`] instantiated with the provided field elements where `elements[0]` + /// is taken as the first felt and `elements[1]` is taken as the second element. + /// + /// # Errors + /// + /// Returns an error if any of the ID constraints are not met. See the [type + /// documentation](AccountId) for details. + fn try_from(elements: [Felt; 2]) -> Result { + account_id_from_felts(elements) } - - let bits = (int_value & ACCOUNT_STORAGE_MASK) >> ACCOUNT_STORAGE_MASK_SHIFT; - match bits { - PUBLIC | PRIVATE => (), - _ => return Err(AccountError::InvalidAccountStorageMode(format!("0b{bits:b}"))), - }; - - Ok(AccountId(value)) } -impl TryFrom for AccountId { +impl TryFrom<[u8; 15]> for AccountId { type Error = AccountError; - /// Returns an [AccountId] instantiated with the provided field element. + /// Tries to convert a byte array in little-endian order to an [`AccountId`]. /// /// # Errors - /// Returns an error if: - /// - If there are fewer than [AccountId::MIN_ACCOUNT_ONES] in the provided value. - /// - If the provided value contains invalid account ID metadata (i.e., the first 4 bits). - fn try_from(value: Felt) -> Result { - account_id_from_felt(value) - } -} + /// + /// Returns an error if any of the ID constraints are not met. See the [type + /// documentation](AccountId) for details. + fn try_from(bytes: [u8; 15]) -> Result { + // This slice has 8 bytes. + let first_felt_slice = &bytes[..8]; + // This slice has 7 bytes, since the 8th byte will always be zero. + let second_felt_slice = &bytes[8..15]; -impl TryFrom<[u8; 8]> for AccountId { - type Error = AccountError; + // The byte order is little-endian order, so prepending a 0 sets the least significant byte. + let mut second_felt_bytes = [0; 8]; + second_felt_bytes[1..8].copy_from_slice(second_felt_slice); + + let first_felt = + Felt::try_from(first_felt_slice).map_err(AccountError::AccountIdInvalidFieldElement)?; - // Expects little-endian byte order - fn try_from(value: [u8; 8]) -> Result { - let element = parse_felt(&value[..8])?; - Self::try_from(element) + let second_felt = Felt::try_from(second_felt_bytes.as_slice()) + .map_err(AccountError::AccountIdInvalidFieldElement)?; + + Self::try_from([first_felt, second_felt]) } } -impl TryFrom for AccountId { +impl TryFrom for AccountId { type Error = AccountError; - fn try_from(value: u64) -> Result { - let element = parse_felt(&value.to_le_bytes())?; - Self::try_from(element) + /// Tries to convert a u128 into an [`AccountId`]. + /// + /// # Errors + /// + /// Returns an error if any of the ID constraints are not met. See the [type + /// documentation](AccountId) for details. + fn try_from(int: u128) -> Result { + let little_endian_bytes = int.to_le_bytes(); + let mut bytes: [u8; 15] = [0; 15]; + + // Swap the positions of the Felts to match what the TryFrom<[u8; 15]> impl expects. + // This copies the first felt's 8 bytes. + bytes[..8].copy_from_slice(&little_endian_bytes[8..]); + // This copies the second felt's 7 bytes. The least significant byte is zero and is + // therefore skipped. + bytes[8..].copy_from_slice(&little_endian_bytes[1..8]); + + Self::try_from(bytes) } } @@ -472,17 +621,18 @@ impl TryFrom for AccountId { impl Serializable for AccountId { fn write_into(&self, target: &mut W) { - self.0.write_into(target); + let bytes: [u8; 15] = (*self).into(); + bytes.write_into(target); } fn get_size_hint(&self) -> usize { - self.0.get_size_hint() + Self::SERIALIZED_SIZE } } impl Deserializable for AccountId { fn read_from(source: &mut R) -> Result { - Felt::read_from(source)? + <[u8; 15]>::read_from(source)? .try_into() .map_err(|err: AccountError| DeserializationError::InvalidValue(err.to_string())) } @@ -490,240 +640,293 @@ impl Deserializable for AccountId { // HELPER FUNCTIONS // ================================================================================================ -fn parse_felt(bytes: &[u8]) -> Result { - Felt::try_from(bytes).map_err(AccountError::AccountIdInvalidFieldElement) + +/// Returns an [AccountId] instantiated with the provided field elements. +/// +/// # Errors +/// +/// Returns an error if any of the ID constraints are not met. See the See the [type +/// documentation](AccountId) for details. +fn account_id_from_felts(elements: [Felt; 2]) -> Result { + validate_first_felt(elements[0])?; + validate_second_felt(elements[1])?; + + Ok(AccountId { + first_felt: elements[0], + second_felt: elements[1], + }) } -/// Returns the digest of two hashing permutations over the seed, code commitment, storage -/// commitment and padding. -pub(super) fn compute_digest( - seed: Word, - code_commitment: Digest, - storage_commitment: Digest, -) -> Digest { - let mut elements = Vec::with_capacity(16); - elements.extend(seed); - elements.extend(*code_commitment); - elements.extend(*storage_commitment); - elements.resize(16, ZERO); - Hasher::hash_elements(&elements) +/// Checks that the first felt: +/// - has known values for metadata (storage mode, type and version). +pub(super) fn validate_first_felt( + first_felt: Felt, +) -> Result<(AccountType, AccountStorageMode, AccountIdVersion), AccountError> { + let first_felt = first_felt.as_int(); + + // Validate storage bits. + let storage_mode = extract_storage_mode(first_felt)?; + + // Validate version bits. + let version = extract_version(first_felt)?; + + let account_type = extract_type(first_felt); + + Ok((account_type, storage_mode, version)) } -/// Given a [Digest] returns its proof-of-work. -pub(super) fn digest_pow(digest: Digest) -> u32 { - digest.as_elements()[3].as_int().trailing_zeros() +/// Checks that the second felt: +/// - has an anchor_epoch that is not [`u16::MAX`]. +/// - has its lower 8 bits set to zero. +fn validate_second_felt(second_felt: Felt) -> Result<(), AccountError> { + let second_felt = second_felt.as_int(); + + if extract_anchor_epoch(second_felt) == u16::MAX { + return Err(AccountError::AssumptionViolated( + "TODO: Make proper error: second felt epoch must be less than 2^16".into(), + )); + } + + // Validate lower 8 bits of second felt are zero. + if second_felt & 0xff != 0 { + return Err(AccountError::AssumptionViolated( + "TODO: Make proper error: second felt lower 8 bits must be zero".into(), + )); + } + + Ok(()) } -/// Returns true if an account with this ID is a regular account. -fn is_regular_account(account_id: u64) -> bool { - let account_type = account_id.into(); - matches!( - account_type, - AccountType::RegularAccountUpdatableCode | AccountType::RegularAccountImmutableCode - ) +pub(crate) fn extract_storage_mode(first_felt: u64) -> Result { + let bits = (first_felt & AccountId::STORAGE_MODE_MASK) >> AccountId::STORAGE_MODE_SHIFT; + match bits { + PUBLIC => Ok(AccountStorageMode::Public), + PRIVATE => Ok(AccountStorageMode::Private), + _ => Err(AccountError::InvalidAccountStorageMode(format!("0b{bits:b}"))), + } } -// TESTING -// ================================================================================================ +pub(crate) fn extract_version(first_felt: u64) -> Result { + let bits = first_felt & AccountId::VERSION_MASK; + let version = bits.try_into().expect("TODO"); + match version { + AccountIdVersion::VERSION_0_NUMBER => Ok(AccountIdVersion::VERSION_0), + other => Err(AccountError::AssumptionViolated(format!( + "TODO: Error. Unexpected version {other}" + ))), + } +} -#[cfg(any(feature = "testing", test))] -pub mod testing { - use super::{ - AccountStorageMode, AccountType, ACCOUNT_STORAGE_MASK_SHIFT, ACCOUNT_TYPE_MASK_SHIFT, - }; +pub(crate) const fn extract_type(first_felt: u64) -> AccountType { + let bits = (first_felt & AccountId::TYPE_MASK) >> AccountId::TYPE_SHIFT; + match bits { + REGULAR_ACCOUNT_UPDATABLE_CODE => AccountType::RegularAccountUpdatableCode, + REGULAR_ACCOUNT_IMMUTABLE_CODE => AccountType::RegularAccountImmutableCode, + FUNGIBLE_FAUCET => AccountType::FungibleFaucet, + NON_FUNGIBLE_FAUCET => AccountType::NonFungibleFaucet, + _ => { + // SAFETY: type mask contains only 2 bits and we've covered all 4 possible options. + unreachable!() + }, + } +} - // CONSTANTS - // -------------------------------------------------------------------------------------------- +fn extract_anchor_epoch(second_felt: u64) -> u16 { + ((second_felt & AccountId::ANCHOR_EPOCH_MASK) >> AccountId::ANCHOR_EPOCH_SHIFT) as u16 +} - // REGULAR ACCOUNTS - OFF-CHAIN - pub const ACCOUNT_ID_SENDER: u64 = account_id( - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - 0b0001_1111, - ); - pub const ACCOUNT_ID_OFF_CHAIN_SENDER: u64 = account_id( - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Private, - 0b0010_1111, - ); - pub const ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN: u64 = account_id( - AccountType::RegularAccountUpdatableCode, - AccountStorageMode::Private, - 0b0011_1111, - ); - // REGULAR ACCOUNTS - ON-CHAIN - pub const ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN: u64 = account_id( - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Public, - 0b0001_1111, - ); - pub const ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN_2: u64 = account_id( - AccountType::RegularAccountImmutableCode, - AccountStorageMode::Public, - 0b0010_1111, - ); - pub const ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN: u64 = account_id( - AccountType::RegularAccountUpdatableCode, - AccountStorageMode::Public, - 0b0011_1111, - ); - pub const ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2: u64 = account_id( - AccountType::RegularAccountUpdatableCode, - AccountStorageMode::Public, - 0b0100_1111, - ); - - // FUNGIBLE TOKENS - OFF-CHAIN - pub const ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN: u64 = - account_id(AccountType::FungibleFaucet, AccountStorageMode::Private, 0b0001_1111); - // FUNGIBLE TOKENS - ON-CHAIN - pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN: u64 = - account_id(AccountType::FungibleFaucet, AccountStorageMode::Public, 0b0001_1111); - pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1: u64 = - account_id(AccountType::FungibleFaucet, AccountStorageMode::Public, 0b0010_1111); - pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2: u64 = - account_id(AccountType::FungibleFaucet, AccountStorageMode::Public, 0b0011_1111); - pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3: u64 = - account_id(AccountType::FungibleFaucet, AccountStorageMode::Public, 0b0100_1111); - - // NON-FUNGIBLE TOKENS - OFF-CHAIN - pub const ACCOUNT_ID_INSUFFICIENT_ONES: u64 = - account_id(AccountType::NonFungibleFaucet, AccountStorageMode::Private, 0b0000_0000); // invalid - pub const ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN: u64 = - account_id(AccountType::NonFungibleFaucet, AccountStorageMode::Private, 0b0001_1111); - // NON-FUNGIBLE TOKENS - ON-CHAIN - pub const ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN: u64 = - account_id(AccountType::NonFungibleFaucet, AccountStorageMode::Public, 0b0010_1111); - pub const ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1: u64 = - account_id(AccountType::NonFungibleFaucet, AccountStorageMode::Public, 0b0011_1111); - - // UTILITIES - // -------------------------------------------------------------------------------------------- +/// Shapes the second felt so it meets the requirements of the account ID, by overwriting the +/// upper 16 bits with the epoch and setting the lower 8 bits to zero. +fn shape_second_felt(second_felt: Felt, anchor_epoch: u16) -> Felt { + if anchor_epoch == u16::MAX { + unimplemented!("TODO: Return error"); + } - pub const fn account_id( - account_type: AccountType, - storage_mode: AccountStorageMode, - rest: u64, - ) -> u64 { - let mut id = 0; + let mut second_felt = second_felt.as_int(); - id ^= (storage_mode as u64) << ACCOUNT_STORAGE_MASK_SHIFT; - id ^= (account_type as u64) << ACCOUNT_TYPE_MASK_SHIFT; - id ^= rest; + // Clear upper 16 epoch bits and the lower 8 bits. + second_felt &= 0x0000_ffff_ffff_ff00; - id + // Set the upper 16 anchor epoch bits. + second_felt |= (anchor_epoch as u64) << AccountId::ANCHOR_EPOCH_SHIFT; + + // SAFETY: We disallow u16::MAX which would be all 1 bits, so at least one of the most + // significant bits will always be zero. + Felt::try_from(second_felt).expect("epoch is never all ones so felt should be valid") +} + +// COMMON TRAIT IMPLS +// ================================================================================================ + +impl PartialOrd for AccountId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for AccountId { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + u128::from(*self).cmp(&u128::from(*other)) + } +} + +impl fmt::Display for AccountId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_hex()) } } +/// Returns the digest of two hashing permutations over the seed, code commitment, storage +/// commitment and padding. +pub(super) fn compute_digest( + seed: Word, + code_commitment: Digest, + storage_commitment: Digest, + anchor_block_hash: Digest, +) -> Digest { + let mut elements = Vec::with_capacity(16); + elements.extend(seed); + elements.extend(*code_commitment); + elements.extend(*storage_commitment); + elements.extend(*anchor_block_hash); + Hasher::hash_elements(&elements) +} + // TESTS // ================================================================================================ #[cfg(test)] mod tests { - use miden_crypto::utils::{Deserializable, Serializable}; - use super::{ - testing::*, AccountId, AccountStorageMode, AccountType, ACCOUNT_ISFAUCET_MASK, - ACCOUNT_TYPE_MASK_SHIFT, FUNGIBLE_FAUCET, NON_FUNGIBLE_FAUCET, - REGULAR_ACCOUNT_IMMUTABLE_CODE, REGULAR_ACCOUNT_UPDATABLE_CODE, + use super::*; + use crate::testing::account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, + ACCOUNT_ID_OFF_CHAIN_SENDER, ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, }; #[test] - fn test_account_id() { - use crate::accounts::AccountId; + fn test_account_id_from_seed_with_epoch() { + let code_commitment: Digest = Digest::default(); + let storage_commitment: Digest = Digest::default(); + let anchor_block_hash: Digest = Digest::default(); - for account_type in [ - AccountType::RegularAccountImmutableCode, - AccountType::RegularAccountUpdatableCode, - AccountType::NonFungibleFaucet, + let seed = AccountId::compute_account_seed( + [10; 32], AccountType::FungibleFaucet, - ] { - for storage_mode in [AccountStorageMode::Public, AccountStorageMode::Private] { - let acc = AccountId::try_from(account_id(account_type, storage_mode, 0b1111_1111)) - .unwrap(); - assert_eq!(acc.account_type(), account_type); - assert_eq!(acc.storage_mode(), storage_mode); - } - } - } + AccountStorageMode::Public, + AccountIdVersion::VERSION_0, + code_commitment, + storage_commitment, + anchor_block_hash, + ) + .unwrap(); - #[test] - fn test_account_id_from_hex_and_back() { - for account_id in [ - ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, - ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, - ] { - let acc = AccountId::try_from(account_id).expect("Valid account ID"); - assert_eq!(acc, AccountId::from_hex(&acc.to_hex()).unwrap()); + for anchor_epoch in [0, u16::MAX - 1, 5000] { + let anchor = AccountIdAnchor::new_unchecked(anchor_epoch, anchor_block_hash); + let id = AccountId::new(seed, anchor, code_commitment, storage_commitment).unwrap(); + assert_eq!(id.anchor_epoch(), anchor_epoch, "failed for account id: {id}"); } } #[test] - fn test_account_id_serde() { - let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN) - .expect("Valid account ID"); - assert_eq!(account_id, AccountId::read_from_bytes(&account_id.to_bytes()).unwrap()); + fn account_id_from_felts_with_high_pop_count() { + let valid_second_felt = Felt::try_from(0xfffe_ffff_ffff_ff00u64).unwrap(); + let valid_first_felt = Felt::try_from(0x7fff_ffff_ffff_ff00u64).unwrap(); - let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN) - .expect("Valid account ID"); - assert_eq!(account_id, AccountId::read_from_bytes(&account_id.to_bytes()).unwrap()); + let id1 = AccountId::new_unchecked([valid_first_felt, valid_second_felt]); + assert_eq!(id1.account_type(), AccountType::RegularAccountImmutableCode); + assert_eq!(id1.storage_mode(), AccountStorageMode::Public); + assert_eq!(id1.version(), AccountIdVersion::VERSION_0); + assert_eq!(id1.anchor_epoch(), u16::MAX - 1); + } - let account_id = - AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).expect("Valid account ID"); - assert_eq!(account_id, AccountId::read_from_bytes(&account_id.to_bytes()).unwrap()); + #[test] + fn account_id_construction() { + // Use the highest possible input to check if the constructed id is a valid Felt in that + // scenario. + // Use the lowest possible input to check whether the constructor produces valid IDs with + // all-zeroes input. + for input in [[0xff; 15], [0; 15]] { + for account_type in [ + AccountType::FungibleFaucet, + AccountType::NonFungibleFaucet, + AccountType::RegularAccountImmutableCode, + AccountType::RegularAccountUpdatableCode, + ] { + for storage_mode in [AccountStorageMode::Private, AccountStorageMode::Public] { + let id = AccountId::new_dummy(input, account_type, storage_mode); + assert_eq!(id.account_type(), account_type); + assert_eq!(id.storage_mode(), storage_mode); + assert_eq!(id.version(), AccountIdVersion::VERSION_0); + assert_eq!(id.anchor_epoch(), 0); - let account_id = AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN) - .expect("Valid account ID"); - assert_eq!(account_id, AccountId::read_from_bytes(&account_id.to_bytes()).unwrap()); + // Do a serialization roundtrip to ensure validity. + let serialized_id = id.to_bytes(); + AccountId::read_from_bytes(&serialized_id).unwrap(); + assert_eq!(serialized_id.len(), AccountId::SERIALIZED_SIZE); + } + } + } } #[test] - fn test_account_id_account_type() { - let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN) - .expect("Valid account ID"); - - let account_type: AccountType = ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN.into(); - assert_eq!(account_type, account_id.account_type()); + fn account_id_prefix_serialization_compatibility() { + // Ensure that an AccountIdPrefix can be read from the serialized bytes of an AccountId. + let account_id = AccountId::try_from(ACCOUNT_ID_OFF_CHAIN_SENDER).unwrap(); + let id_bytes = account_id.to_bytes(); + let deserialized_prefix = AccountIdPrefix::read_from_bytes(&id_bytes).unwrap(); + assert_eq!(account_id.prefix(), deserialized_prefix); - let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN) - .expect("Valid account ID"); - let account_type: AccountType = ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN.into(); - assert_eq!(account_type, account_id.account_type()); + // Ensure AccountId and AccountIdPrefix's hex representation are compatible. + assert!(account_id.to_hex().starts_with(&account_id.prefix().to_string())); + } - let account_id = - AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).expect("Valid account ID"); - let account_type: AccountType = ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.into(); - assert_eq!(account_type, account_id.account_type()); + // CONVERSION TESTS + // ================================================================================================ - let account_id = AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN) - .expect("Valid account ID"); - let account_type: AccountType = ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN.into(); - assert_eq!(account_type, account_id.account_type()); + #[test] + fn test_account_id_conversion_roundtrip() { + for (idx, account_id) in [ + ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, + ACCOUNT_ID_OFF_CHAIN_SENDER, + ] + .into_iter() + .enumerate() + { + let id = AccountId::try_from(account_id).expect("account ID should be valid"); + assert_eq!(id, AccountId::from_hex(&id.to_hex()).unwrap(), "failed in {idx}"); + assert_eq!(id, AccountId::try_from(<[u8; 15]>::from(id)).unwrap(), "failed in {idx}"); + assert_eq!(id, AccountId::try_from(u128::from(id)).unwrap(), "failed in {idx}"); + assert_eq!(account_id, u128::from(id), "failed in {idx}"); + } } #[test] fn test_account_id_tag_identifiers() { let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN) - .expect("Valid account ID"); + .expect("valid account ID"); assert!(account_id.is_regular_account()); assert_eq!(account_id.account_type(), AccountType::RegularAccountImmutableCode); assert!(account_id.is_public()); let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN) - .expect("Valid account ID"); + .expect("valid account ID"); assert!(account_id.is_regular_account()); assert_eq!(account_id.account_type(), AccountType::RegularAccountUpdatableCode); assert!(!account_id.is_public()); let account_id = - AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).expect("Valid account ID"); + AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).expect("valid account ID"); assert!(account_id.is_faucet()); assert_eq!(account_id.account_type(), AccountType::FungibleFaucet); assert!(account_id.is_public()); let account_id = AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN) - .expect("Valid account ID"); + .expect("valid account ID"); assert!(account_id.is_faucet()); assert_eq!(account_id.account_type(), AccountType::NonFungibleFaucet); assert!(!account_id.is_public()); @@ -733,40 +936,14 @@ mod tests { /// normal. #[test] fn test_account_id_faucet_bit() { + const ACCOUNT_IS_FAUCET_MASK: u64 = 0b10; + // faucets have a bit set - assert_ne!((FUNGIBLE_FAUCET << ACCOUNT_TYPE_MASK_SHIFT) & ACCOUNT_ISFAUCET_MASK, 0); - assert_ne!((NON_FUNGIBLE_FAUCET << ACCOUNT_TYPE_MASK_SHIFT) & ACCOUNT_ISFAUCET_MASK, 0); + assert_ne!((FUNGIBLE_FAUCET) & ACCOUNT_IS_FAUCET_MASK, 0); + assert_ne!((NON_FUNGIBLE_FAUCET) & ACCOUNT_IS_FAUCET_MASK, 0); // normal accounts do not have the faucet bit set - assert_eq!( - (REGULAR_ACCOUNT_IMMUTABLE_CODE << ACCOUNT_TYPE_MASK_SHIFT) & ACCOUNT_ISFAUCET_MASK, - 0 - ); - assert_eq!( - (REGULAR_ACCOUNT_UPDATABLE_CODE << ACCOUNT_TYPE_MASK_SHIFT) & ACCOUNT_ISFAUCET_MASK, - 0 - ); - } - - #[test] - fn account_id_construction() { - // Use the highest possible input to check if the constructed id is a valid Felt in that - // scenario. - // Use the lowest possible input to check whether the constructor satisfies - // MIN_ACCOUNT_ONES. - for input in [[0xff; 8], [0; 8]] { - for account_type in [ - AccountType::FungibleFaucet, - AccountType::NonFungibleFaucet, - AccountType::RegularAccountImmutableCode, - AccountType::RegularAccountUpdatableCode, - ] { - for storage_mode in [AccountStorageMode::Private, AccountStorageMode::Public] { - let id = AccountId::new_with_type_and_mode(input, account_type, storage_mode); - // Do a serialization roundtrip to ensure validity. - AccountId::read_from_bytes(&id.to_bytes()).unwrap(); - } - } - } + assert_eq!((REGULAR_ACCOUNT_IMMUTABLE_CODE) & ACCOUNT_IS_FAUCET_MASK, 0); + assert_eq!((REGULAR_ACCOUNT_UPDATABLE_CODE) & ACCOUNT_IS_FAUCET_MASK, 0); } } diff --git a/objects/src/accounts/account_id_anchor.rs b/objects/src/accounts/account_id_anchor.rs new file mode 100644 index 000000000..542d797de --- /dev/null +++ b/objects/src/accounts/account_id_anchor.rs @@ -0,0 +1,118 @@ +use crate::{block::block_epoch_from_number, AccountError, BlockHeader, Digest, EMPTY_WORD}; + +// ACCOUNT ID ANCHOR +// ================================================================================================ + +/// The anchor of an [`AccountId`](crate::accounts::AccountId). See the type's documentation for +/// details on anchors. +/// +/// This type is recommended to be created from a reference to a [`BlockHeader`] via the `TryFrom` +/// impl. +/// +/// # Constraints +/// +/// This type enforces the following constraints. +/// - The `anchor_block_number` % 2^[`BlockHeader::EPOCH_LENGTH_EXPONENT`] must be zero. In other +/// words, the block number must a multiple of 2^[`BlockHeader::EPOCH_LENGTH_EXPONENT`]. +/// - The epoch derived from the `anchor_block_number` must be strictly less than [`u16::MAX`]. +#[derive(Debug, Clone, Copy)] +pub struct AccountIdAnchor { + epoch: u16, + block_hash: Digest, +} + +impl AccountIdAnchor { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// A "pre-genesis" [`AccountIdAnchor`] which can be used to anchor accounts created in the + /// genesis block. + pub const PRE_GENESIS: Self = Self { + epoch: 0, + block_hash: Digest::new(EMPTY_WORD), + }; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`AccountIdAnchor`] from the provided `anchor_block_number` and + /// `anchor_block_hash`. + /// + /// # Errors + /// + /// Returns an error if any of the anchor constraints are not met. See the [type + /// documentation](AccountIdAnchor) for details. + pub fn new(anchor_block_number: u32, anchor_block_hash: Digest) -> Result { + if anchor_block_number & 0x0000_ffff != 0 { + return Err(AccountError::AssumptionViolated(format!( + "TODO: Make proper error: anchor block must be an epoch block, i.e. its block number must be a multiple of 2^{}", + BlockHeader::EPOCH_LENGTH_EXPONENT))); + } + + let anchor_epoch = block_epoch_from_number(anchor_block_number); + + if anchor_epoch == u16::MAX { + return Err(AccountError::AssumptionViolated(format!( + "TODO: Make proper error: anchor epoch cannot be {}", + u16::MAX + ))); + } + + Ok(Self { + epoch: anchor_epoch, + block_hash: anchor_block_hash, + }) + } + + /// Creates a new [`AccountIdAnchor`] from the provided `anchor_epoch` and `anchor_block_hash` + /// without validation. + /// + /// # Warning + /// + /// The caller must ensure validity of the `anchor_epoch`, in particular the correctness of the + /// relationship between the `anchor_epoch` and the provided `anchor_block_hash`. + /// + /// # Panics + /// + /// If debug_assertions are enabled (e.g. in debug mode), this function panics if the + /// `anchor_epoch` is [`u16::MAX`]. + pub fn new_unchecked(anchor_epoch: u16, anchor_block_hash: Digest) -> Self { + debug_assert_ne!(anchor_epoch, u16::MAX, "anchor epoch cannot be u16::MAX"); + + Self { + epoch: anchor_epoch, + block_hash: anchor_block_hash, + } + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the epoch of this anchor. + pub fn epoch(self) -> u16 { + self.epoch + } + + /// Returns the block hash of this anchor. + pub fn block_hash(self) -> Digest { + self.block_hash + } +} + +// CONVERSIONS TO ACCOUNT ID ANCHOR +// ================================================================================================ + +impl TryFrom<&BlockHeader> for AccountIdAnchor { + type Error = AccountError; + + /// Extracts the [`BlockHeader::block_num`] and [`BlockHeader::hash`] from the provided + /// `block_header` and tries to convert it to an [`AccountIdAnchor`]. + /// + /// # Errors + /// + /// Returns an error if any of the anchor constraints are not met. See the [type + /// documentation](AccountIdAnchor) for details. + fn try_from(block_header: &BlockHeader) -> Result { + Self::new(block_header.block_num(), block_header.hash()) + } +} diff --git a/objects/src/accounts/account_id_prefix.rs b/objects/src/accounts/account_id_prefix.rs new file mode 100644 index 000000000..b198741c4 --- /dev/null +++ b/objects/src/accounts/account_id_prefix.rs @@ -0,0 +1,263 @@ +use alloc::string::ToString; +use core::fmt; + +use miden_crypto::utils::ByteWriter; +use vm_core::{ + utils::{ByteReader, Deserializable, Serializable}, + Felt, +}; +use vm_processor::DeserializationError; + +use super::account_id; +use crate::{ + accounts::{ + account_id::validate_first_felt, AccountIdVersion, AccountStorageMode, AccountType, + }, + AccountError, +}; + +// ACCOUNT ID PREFIX +// ================================================================================================ + +/// The first felt of an [`AccountId`][id], i.e. its prefix. +/// +/// See the type's documentation for details. +/// +/// The serialization formats of [`AccountIdPrefix`] and [`AccountId`][id] are compatible. In +/// particular, a prefix can be deserialized from the serialized bytes of a full id. +/// +/// [id]: crate::accounts::AccountId +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct AccountIdPrefix { + first_felt: Felt, +} + +impl AccountIdPrefix { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The serialized size of an [`AccountIdPrefix`] in bytes. + pub const SERIALIZED_SIZE: usize = 8; + + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Constructs a new [`AccountIdPrefix`] from the given `first_felt` without checking its + /// validity. + /// + /// # Warning + /// + /// Validity of the ID prefix must be ensured by the caller. An invalid ID may lead to panics. + /// + /// # Panics + /// + /// If debug_assertions are enabled (e.g. in debug mode), this function panics if the given + /// felt is invalid according to the constraints in the + /// [`AccountId`](crate::accounts::AccountId) documentation. + pub fn new_unchecked(first_felt: Felt) -> Self { + // Panic on invalid felts in debug mode. + if cfg!(debug_assertions) { + validate_first_felt(first_felt) + .expect("AccountIdPrefix::new_unchecked called with invalid first felt"); + } + + AccountIdPrefix { first_felt } + } + + /// Constructs a new [`AccountIdPrefix`] from the given `first_felt` and checks its validity. + /// + /// # Errors + /// + /// Returns an error if any of the ID constraints of the first felt are not met. See the + /// [`AccountId`](crate::accounts::AccountId) type documentation for details. + pub fn new(first_felt: Felt) -> Result { + validate_first_felt(first_felt)?; + + Ok(AccountIdPrefix { first_felt }) + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the type of this account ID. + pub const fn account_type(&self) -> AccountType { + account_id::extract_type(self.first_felt.as_int()) + } + + /// Returns true if an account with this ID is a faucet (can issue assets). + pub fn is_faucet(&self) -> bool { + self.account_type().is_faucet() + } + + /// Returns true if an account with this ID is a regular account. + pub fn is_regular_account(&self) -> bool { + self.account_type().is_regular_account() + } + + /// Returns the storage mode of this account ID. + pub fn storage_mode(&self) -> AccountStorageMode { + account_id::extract_storage_mode(self.first_felt.as_int()) + .expect("account id prefix should have been constructed with a valid storage mode") + } + + /// Returns true if an account with this ID is a public account. + pub fn is_public(&self) -> bool { + self.storage_mode() == AccountStorageMode::Public + } + + /// Returns the version of this account ID. + pub fn version(&self) -> AccountIdVersion { + account_id::extract_version(self.first_felt.as_int()) + .expect("account id prefix should have been constructed with a valid version") + } +} + +// CONVERSIONS FROM ACCOUNT ID PREFIX +// ================================================================================================ + +impl From for Felt { + fn from(id: AccountIdPrefix) -> Self { + id.first_felt + } +} + +impl From for [u8; 8] { + fn from(id: AccountIdPrefix) -> Self { + let mut result = [0_u8; 8]; + result[..8].copy_from_slice(&id.first_felt.as_int().to_le_bytes()); + result + } +} + +impl From for u64 { + fn from(id: AccountIdPrefix) -> Self { + id.first_felt.as_int() + } +} + +// CONVERSIONS TO ACCOUNT ID PREFIX +// ================================================================================================ + +impl TryFrom<[u8; 8]> for AccountIdPrefix { + type Error = AccountError; + + /// Tries to convert a byte array in little-endian order to an [`AccountIdPrefix`]. + /// + /// # Errors + /// + /// Returns an error if any of the ID constraints of the first felt are not met. See the + /// [`AccountId`](crate::accounts::AccountId) type documentation for details. + fn try_from(value: [u8; 8]) -> Result { + let element = + Felt::try_from(&value[..8]).map_err(AccountError::AccountIdInvalidFieldElement)?; + Self::new(element) + } +} + +impl TryFrom for AccountIdPrefix { + type Error = AccountError; + + /// Tries to convert a `u64` into an [`AccountIdPrefix`]. + /// + /// # Errors + /// + /// Returns an error if any of the ID constraints of the first felt are not met. See the + /// [`AccountId`](crate::accounts::AccountId) type documentation for details. + fn try_from(value: u64) -> Result { + let element = Felt::try_from(value.to_le_bytes().as_slice()) + .map_err(AccountError::AccountIdInvalidFieldElement)?; + Self::new(element) + } +} + +impl TryFrom for AccountIdPrefix { + type Error = AccountError; + + /// Returns an [`AccountIdPrefix`] instantiated with the provided field . + /// + /// # Errors + /// + /// Returns an error if any of the ID constraints of the first felt are not met. See the + /// [`AccountId`](crate::accounts::AccountId) type documentation for details. + fn try_from(element: Felt) -> Result { + Self::new(element) + } +} + +// COMMON TRAIT IMPLS +// ================================================================================================ + +impl PartialOrd for AccountIdPrefix { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for AccountIdPrefix { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.first_felt.as_int().cmp(&other.first_felt.as_int()) + } +} + +impl fmt::Display for AccountIdPrefix { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{:016x}", self.first_felt.as_int()) + } +} + +// SERIALIZATION +// ================================================================================================ + +impl Serializable for AccountIdPrefix { + fn write_into(&self, target: &mut W) { + let bytes: [u8; 8] = (*self).into(); + bytes.write_into(target); + } + + fn get_size_hint(&self) -> usize { + Self::SERIALIZED_SIZE + } +} + +impl Deserializable for AccountIdPrefix { + fn read_from(source: &mut R) -> Result { + <[u8; 8]>::read_from(source)? + .try_into() + .map_err(|err: AccountError| DeserializationError::InvalidValue(err.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::accounts::AccountId; + + #[test] + fn account_id_prefix_construction() { + // Use the highest possible input to check if the constructed id is a valid Felt in that + // scenario. + // Use the lowest possible input to check whether the constructor produces valid IDs with + // all-zeroes input. + for input in [[0xff; 15], [0; 15]] { + for account_type in [ + AccountType::FungibleFaucet, + AccountType::NonFungibleFaucet, + AccountType::RegularAccountImmutableCode, + AccountType::RegularAccountUpdatableCode, + ] { + for storage_mode in [AccountStorageMode::Private, AccountStorageMode::Public] { + let id = AccountId::new_dummy(input, account_type, storage_mode); + let prefix = id.prefix(); + assert_eq!(prefix.account_type(), account_type); + assert_eq!(prefix.storage_mode(), storage_mode); + assert_eq!(prefix.version(), AccountIdVersion::VERSION_0); + + // Do a serialization roundtrip to ensure validity. + let serialized_prefix = prefix.to_bytes(); + AccountIdPrefix::read_from_bytes(&serialized_prefix).unwrap(); + assert_eq!(serialized_prefix.len(), AccountIdPrefix::SERIALIZED_SIZE); + } + } + } + } +} diff --git a/objects/src/accounts/builder/mod.rs b/objects/src/accounts/builder/mod.rs index 1b9770419..f419b5d44 100644 --- a/objects/src/accounts/builder/mod.rs +++ b/objects/src/accounts/builder/mod.rs @@ -5,10 +5,10 @@ use vm_processor::Digest; use crate::{ accounts::{ - Account, AccountCode, AccountComponent, AccountId, AccountStorage, AccountStorageMode, - AccountType, + Account, AccountCode, AccountComponent, AccountId, AccountIdAnchor, AccountIdVersion, + AccountStorage, AccountStorageMode, AccountType, }, - assets::{Asset, AssetVault}, + assets::AssetVault, AccountError, Felt, Word, }; @@ -24,23 +24,31 @@ use crate::{ /// By default, the builder is initialized with: /// - The `account_type` set to [`AccountType::RegularAccountUpdatableCode`]. /// - The `storage_mode` set to [`AccountStorageMode::Private`]. +/// - The `version` set to [`AccountIdVersion::VERSION_0`]. /// /// The methods that are required to be called are: /// /// - [`AccountBuilder::init_seed`], /// - [`AccountBuilder::with_component`], which must be called at least once. +/// - [`AccountBuilder::anchor`]. +/// +/// The latter methods set the anchor block hash and epoch which will be used for the generation of +/// the account's ID. See [`AccountId`] for details on its generation and anchor blocks. /// /// Under the `testing` feature, it is possible to: /// - Change the `nonce` to build an existing account. -/// - Set assets which will be placed in the account's vault. +/// - Add assets to the account's vault, however this will only succeed when using +/// [`AccountBuilder::build_existing`]. #[derive(Debug, Clone)] pub struct AccountBuilder { #[cfg(any(feature = "testing", test))] - assets: Vec, + assets: Vec, components: Vec, account_type: AccountType, storage_mode: AccountStorageMode, + id_anchor: Option, init_seed: Option<[u8; 32]>, + id_version: AccountIdVersion, } impl AccountBuilder { @@ -51,8 +59,10 @@ impl AccountBuilder { assets: vec![], components: vec![], init_seed: None, + id_anchor: None, account_type: AccountType::RegularAccountUpdatableCode, storage_mode: AccountStorageMode::Private, + id_version: AccountIdVersion::VERSION_0, } } @@ -65,6 +75,18 @@ impl AccountBuilder { self } + /// Sets the [`AccountIdAnchor`] used for the generation of the account ID. + pub fn anchor(mut self, anchor: AccountIdAnchor) -> Self { + self.id_anchor = Some(anchor); + self + } + + /// Sets the [`AccountIdVersion`] of the account ID. + pub fn version(mut self, version: AccountIdVersion) -> Self { + self.id_version = version; + self + } + /// Sets the type of the account. pub fn account_type(mut self, account_type: AccountType) -> Self { self.account_type = account_type; @@ -120,24 +142,25 @@ impl AccountBuilder { fn grind_account_id( &self, init_seed: [u8; 32], + version: AccountIdVersion, code_commitment: Digest, storage_commitment: Digest, - ) -> Result<(AccountId, Word), AccountError> { - let seed = AccountId::get_account_seed( + block_hash: Digest, + ) -> Result { + let seed = AccountId::compute_account_seed( init_seed, self.account_type, self.storage_mode, + version, code_commitment, storage_commitment, + block_hash, ) .map_err(|err| { AccountError::BuildError("account seed generation failed".into(), Some(Box::new(err))) })?; - let account_id = AccountId::new(seed, code_commitment, storage_commitment) - .expect("get_account_seed should provide a suitable seed"); - - Ok((account_id, seed)) + Ok(seed) } /// Builds an [`Account`] out of the configured builder. @@ -158,6 +181,10 @@ impl AccountBuilder { pub fn build(self) -> Result<(Account, Word), AccountError> { let (init_seed, vault, code, storage) = self.build_inner()?; + let id_anchor = self + .id_anchor + .ok_or_else(|| AccountError::BuildError("anchor must be set".into(), None))?; + #[cfg(any(feature = "testing", test))] if !vault.is_empty() { return Err(AccountError::BuildError( @@ -166,8 +193,16 @@ impl AccountBuilder { )); } - let (account_id, seed) = - self.grind_account_id(init_seed, code.commitment(), storage.commitment())?; + let seed = self.grind_account_id( + init_seed, + self.id_version, + code.commitment(), + storage.commitment(), + id_anchor.block_hash(), + )?; + + let account_id = AccountId::new(seed, id_anchor, code.commitment(), storage.commitment()) + .expect("get_account_seed should provide a suitable seed"); debug_assert_eq!(account_id.account_type(), self.account_type); debug_assert_eq!(account_id.storage_mode(), self.storage_mode); @@ -184,7 +219,7 @@ impl AccountBuilder { /// /// Must only be used when using [`Self::build_existing`] instead of [`Self::build`] since new /// accounts must have an empty vault. - pub fn with_assets>(mut self, assets: I) -> Self { + pub fn with_assets>(mut self, assets: I) -> Self { self.assets.extend(assets); self } @@ -198,9 +233,9 @@ impl AccountBuilder { let (init_seed, vault, code, storage) = self.build_inner()?; let account_id = { - let bytes = <[u8; 8]>::try_from(&init_seed[0..8]) - .expect("we should have sliced exactly 8 bytes off"); - AccountId::new_with_type_and_mode(bytes, self.account_type, self.storage_mode) + let bytes = <[u8; 15]>::try_from(&init_seed[0..15]) + .expect("we should have sliced exactly 15 bytes off"); + AccountId::new_dummy(bytes, self.account_type, self.storage_mode) }; Ok(Account::from_parts(account_id, vault, storage, code, Felt::ONE)) @@ -221,6 +256,7 @@ mod tests { use std::sync::LazyLock; use assembly::{Assembler, Library}; + use assert_matches::assert_matches; use vm_core::FieldElement; use super::*; @@ -288,8 +324,13 @@ mod tests { let storage_slot1 = 12; let storage_slot2 = 42; + let anchor_block_hash = Digest::new([Felt::new(42); 4]); + let anchor_block_number = 1 << 16; + let id_anchor = AccountIdAnchor::new(anchor_block_number, anchor_block_hash).unwrap(); + let (account, seed) = Account::builder() .init_seed([5; 32]) + .anchor(id_anchor) .with_component(CustomComponent1 { slot0: storage_slot0 }) .with_component(CustomComponent2 { slot0: storage_slot1, @@ -301,8 +342,13 @@ mod tests { // Account should be new, i.e. nonce = zero. assert_eq!(account.nonce(), Felt::ZERO); - let computed_id = - AccountId::new(seed, account.code.commitment(), account.storage.commitment()).unwrap(); + let computed_id = AccountId::new( + seed, + id_anchor, + account.code.commitment(), + account.storage.commitment(), + ) + .unwrap(); assert_eq!(account.id(), computed_id); // The merged code should have one procedure from each library. @@ -351,15 +397,17 @@ mod tests { fn account_builder_non_empty_vault_on_new_account() { let storage_slot0 = 25; + let anchor = AccountIdAnchor::new_unchecked(5, Digest::default()); let build_error = Account::builder() .init_seed([0xff; 32]) + .anchor(anchor) .with_component(CustomComponent1 { slot0: storage_slot0 }) .with_assets(AssetVault::mock().assets()) .build() .unwrap_err(); - assert!( - matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts") - ) + assert_matches!(build_error, AccountError::BuildError(msg, _) if msg == "account asset vault must be empty on new accounts") } + + // TODO: Test that a BlockHeader with a number which is not a multiple of 2^16 returns an error. } diff --git a/objects/src/accounts/data.rs b/objects/src/accounts/data.rs index 90b994d5c..601be203b 100644 --- a/objects/src/accounts/data.rs +++ b/objects/src/accounts/data.rs @@ -104,11 +104,9 @@ mod tests { use super::AccountData; use crate::{ - accounts::{ - account_id::testing::ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, storage, - Account, AccountCode, AccountId, AuthSecretKey, Felt, Word, - }, + accounts::{storage, Account, AccountCode, AccountId, AuthSecretKey, Felt, Word}, assets::AssetVault, + testing::account_id::ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, }; fn build_account_data() -> AccountData { diff --git a/objects/src/accounts/delta/mod.rs b/objects/src/accounts/delta/mod.rs index bf54d36a8..a09720171 100644 --- a/objects/src/accounts/delta/mod.rs +++ b/objects/src/accounts/delta/mod.rs @@ -283,11 +283,11 @@ mod tests { use super::{AccountDelta, AccountStorageDelta, AccountVaultDelta}; use crate::{ accounts::{ - account_id::testing::ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, delta::AccountUpdateDetails, Account, AccountCode, AccountId, AccountStorage, - AccountType, StorageMapDelta, + AccountStorageMode, AccountType, StorageMapDelta, }, assets::{Asset, AssetVault, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}, + testing::account_id::ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, ONE, ZERO, }; @@ -334,17 +334,24 @@ mod tests { let non_fungible: Asset = NonFungibleAsset::new( &NonFungibleAssetDetails::new( - AccountId::new_dummy([10; 32], AccountType::NonFungibleFaucet), + AccountId::new_dummy( + [10; 15], + AccountType::NonFungibleFaucet, + AccountStorageMode::Public, + ) + .prefix(), vec![6], ) .unwrap(), ) .unwrap() .into(); - let fungible_2: Asset = - FungibleAsset::new(AccountId::new_dummy([10; 32], AccountType::FungibleFaucet), 10) - .unwrap() - .into(); + let fungible_2: Asset = FungibleAsset::new( + AccountId::new_dummy([10; 15], AccountType::FungibleFaucet, AccountStorageMode::Public), + 10, + ) + .unwrap() + .into(); let vault_delta = AccountVaultDelta::from_iters([non_fungible], [fungible_2]); assert_eq!(storage_delta.to_bytes().len(), storage_delta.get_size_hint()); diff --git a/objects/src/accounts/delta/vault.rs b/objects/src/accounts/delta/vault.rs index ea4aae674..ddd4ca4ce 100644 --- a/objects/src/accounts/delta/vault.rs +++ b/objects/src/accounts/delta/vault.rs @@ -504,13 +504,11 @@ pub enum NonFungibleDeltaAction { mod tests { use super::{AccountVaultDelta, Deserializable, Serializable}; use crate::{ - accounts::{ - account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, - }, - AccountId, - }, + accounts::{AccountId, AccountIdPrefix}, assets::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}, + testing::account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + }, }; #[test] @@ -588,11 +586,11 @@ mod tests { /// Creates an [AccountVaultDelta] with an optional [NonFungibleAsset] delta. This delta /// will be added if `Some(true)`, removed for `Some(false)` and missing for `None`. fn create_delta_with_non_fungible( - account_id: AccountId, + account_id_prefix: AccountIdPrefix, added: Option, ) -> AccountVaultDelta { let asset: Asset = NonFungibleAsset::new( - &NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(), + &NonFungibleAssetDetails::new(account_id_prefix, vec![1, 2, 3]).unwrap(), ) .unwrap() .into(); @@ -604,7 +602,7 @@ mod tests { } } - let account_id = NonFungibleAsset::mock_issuer(); + let account_id = NonFungibleAsset::mock_issuer().prefix(); let mut delta_x = create_delta_with_non_fungible(account_id, x); let delta_y = create_delta_with_non_fungible(account_id, y); diff --git a/objects/src/accounts/header.rs b/objects/src/accounts/header.rs index b75f1a77c..f4594f83f 100644 --- a/objects/src/accounts/header.rs +++ b/objects/src/accounts/header.rs @@ -11,7 +11,7 @@ use super::{hash_account, Account, AccountId, Digest, Felt, ZERO}; /// components of the account. /// /// The [AccountHeader] is composed of: -/// - id: the account id ([AccountId]) of the account. +/// - id: the account id ([`AccountId`]) of the account. /// - nonce: the nonce of the account. /// - vault_root: a commitment to the account's vault ([super::AssetVault]). /// - storage_commitment: a commitment to the account's storage ([super::AccountStorage]). @@ -92,7 +92,7 @@ impl AccountHeader { /// This is done by first converting the account header data into an array of Words as follows: /// ```text /// [ - /// [account_id, 0, 0, account_nonce] + /// [account_id_lo, account_id_hi, 0, account_nonce] /// [VAULT_COMMITMENT] /// [STORAGE_COMMITMENT] /// [CODE_COMMITMENT] @@ -101,7 +101,7 @@ impl AccountHeader { /// And then concatenating the resulting elements into a single vector. pub fn as_elements(&self) -> Vec { [ - &[self.id.into(), ZERO, ZERO, self.nonce], + &[self.id.second_felt(), self.id.first_felt(), ZERO, self.nonce], self.vault_root.as_elements(), self.storage_commitment.as_elements(), self.code_commitment.as_elements(), diff --git a/objects/src/accounts/mod.rs b/objects/src/accounts/mod.rs index 0113f368a..6f465131e 100644 --- a/objects/src/accounts/mod.rs +++ b/objects/src/accounts/mod.rs @@ -5,10 +5,13 @@ use crate::{ }; pub mod account_id; -pub use account_id::{ - AccountId, AccountStorageMode, AccountType, ACCOUNT_ISFAUCET_MASK, ACCOUNT_STORAGE_MASK_SHIFT, - ACCOUNT_TYPE_MASK_SHIFT, -}; +pub use account_id::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; + +mod account_id_anchor; +pub use account_id_anchor::AccountIdAnchor; + +mod account_id_prefix; +pub use account_id_prefix::AccountIdPrefix; pub mod auth; @@ -30,7 +33,7 @@ pub use delta::{ }; mod seed; -pub use seed::{get_account_seed, get_account_seed_single}; +pub use seed::compute_account_seed; mod storage; pub use storage::{AccountStorage, AccountStorageHeader, StorageMap, StorageSlot, StorageSlotType}; @@ -59,6 +62,9 @@ pub use data::AccountData; /// Out of the above components account ID is always immutable (once defined it can never be /// changed). Other components may be mutated throughout the lifetime of the account. However, /// account state can be changed only by invoking one of account interface methods. +/// +/// The recommended way to build an account is through an [`AccountBuilder`], which can be +/// instantiated through [`Account::builder`]. See the type's documentation for details. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Account { id: AccountId, @@ -72,24 +78,6 @@ impl Account { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates and returns a new [Account] instantiated with the specified code, storage, and - /// account seed. - /// - /// The returned account has an empty asset vault and the nonce which is initialized to ZERO. - /// - /// # Errors - /// Returns an error if deriving account ID from the specified seed fails. - pub fn new( - seed: Word, - code: AccountCode, - storage: AccountStorage, - ) -> Result { - let id = AccountId::new(seed, code.commitment(), storage.commitment())?; - let vault = AssetVault::default(); - let nonce = ZERO; - Ok(Self { id, vault, storage, code, nonce }) - } - /// Returns an [Account] instantiated with the provided components. pub fn from_parts( id: AccountId, @@ -363,7 +351,8 @@ pub fn hash_account( code_commitment: Digest, ) -> Digest { let mut elements = [ZERO; 16]; - elements[0] = id.into(); + elements[0] = id.second_felt(); + elements[1] = id.first_felt(); elements[3] = nonce; elements[4..8].copy_from_slice(&*vault_root); elements[8..12].copy_from_slice(&*storage_commitment); @@ -403,15 +392,18 @@ mod tests { use vm_processor::Digest; use super::{ - account_id::testing::ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, AccountCode, - AccountDelta, AccountId, AccountStorage, AccountStorageDelta, AccountVaultDelta, + AccountCode, AccountDelta, AccountId, AccountStorage, AccountStorageDelta, + AccountVaultDelta, }; use crate::{ accounts::{ Account, AccountComponent, AccountType, StorageMap, StorageMapDelta, StorageSlot, }, assets::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}, - testing::storage::AccountStorageDeltaBuilder, + testing::{ + account_id::ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, + storage::AccountStorageDeltaBuilder, + }, AccountError, }; diff --git a/objects/src/accounts/seed.rs b/objects/src/accounts/seed.rs index b92878da3..095b97907 100644 --- a/objects/src/accounts/seed.rs +++ b/objects/src/accounts/seed.rs @@ -9,9 +9,9 @@ use std::{ }; use super::{ - account_id::compute_digest, AccountError, AccountId, AccountStorageMode, AccountType, Digest, - Felt, Word, + account_id::compute_digest, AccountError, AccountStorageMode, AccountType, Digest, Felt, Word, }; +use crate::accounts::account_id::{validate_first_felt, AccountIdVersion}; // SEED GENERATORS // -------------------------------------------------------------------------------------------- @@ -19,12 +19,14 @@ use super::{ /// Finds and returns a seed suitable for creating an account ID for the specified account type /// using the provided initial seed as a starting point. Using multi-threading. #[cfg(feature = "concurrent")] -pub fn get_account_seed( +pub fn compute_account_seed( init_seed: [u8; 32], account_type: AccountType, storage_mode: AccountStorageMode, + version: AccountIdVersion, code_commitment: Digest, storage_commitment: Digest, + anchor_block_hash: Digest, ) -> Result { let thread_count = thread::available_parallelism().map_or(1, |v| v.get()); @@ -37,14 +39,16 @@ pub fn get_account_seed( let mut init_seed = init_seed; init_seed[0] = init_seed[0].wrapping_add(count as u8); spawn(move || { - get_account_seed_inner( + compute_account_seed_inner( send, stop, init_seed, account_type, storage_mode, + version, code_commitment, storage_commitment, + anchor_block_hash, ) }); } @@ -57,8 +61,7 @@ pub fn get_account_seed( #[cfg(feature = "log")] ::log::info!( - "Using account seed [pow={}, digest={}, seed={}]", - super::account_id::digest_pow(digest), + "Using account seed [digest={}, seed={}]", log::digest_hex(digest), log::word_hex(seed), ); @@ -67,14 +70,17 @@ pub fn get_account_seed( } #[cfg(feature = "concurrent")] -pub fn get_account_seed_inner( +#[allow(clippy::too_many_arguments)] +fn compute_account_seed_inner( send: Sender<(Digest, Word)>, stop: Arc>, init_seed: [u8; 32], account_type: AccountType, storage_mode: AccountStorageMode, + version: AccountIdVersion, code_commitment: Digest, storage_commitment: Digest, + anchor_block_hash: Digest, ) { let init_seed: Vec<[u8; 8]> = init_seed.chunks(8).map(|chunk| chunk.try_into().unwrap()).collect(); @@ -84,7 +90,8 @@ pub fn get_account_seed_inner( Felt::new(u64::from_le_bytes(init_seed[2])), Felt::new(u64::from_le_bytes(init_seed[3])), ]; - let mut current_digest = compute_digest(current_seed, code_commitment, storage_commitment); + let mut current_digest = + compute_digest(current_seed, code_commitment, storage_commitment, anchor_block_hash); #[cfg(feature = "log")] let mut log = log::Log::start(current_digest, current_seed, account_type, storage_mode); @@ -101,50 +108,60 @@ pub fn get_account_seed_inner( return; } - // check if the seed satisfies the specified account type - if AccountId::validate_seed_digest(¤t_digest).is_ok() { - if let Ok(account_id) = AccountId::try_from(current_digest[0]) { - if account_id.account_type() == account_type - && account_id.storage_mode() == storage_mode - { - #[cfg(feature = "log")] - log.done(current_digest, current_seed, account_id); - - let _ = send.send((current_digest, current_seed)); - return; - }; - } + let first_felt = current_digest.as_elements()[0]; + if let Ok((computed_account_type, computed_storage_mode, computed_version)) = + validate_first_felt(first_felt) + { + if computed_account_type == account_type + && computed_storage_mode == storage_mode + && computed_version == version + { + #[cfg(feature = "log")] + log.done(current_digest, current_seed); + + let _ = send.send((current_digest, current_seed)); + return; + }; } + current_seed = current_digest.into(); - current_digest = compute_digest(current_seed, code_commitment, storage_commitment); + current_digest = + compute_digest(current_seed, code_commitment, storage_commitment, anchor_block_hash); } } #[cfg(not(feature = "concurrent"))] -pub fn get_account_seed( +pub fn compute_account_seed( init_seed: [u8; 32], account_type: AccountType, storage_mode: AccountStorageMode, + version: AccountIdVersion, code_commitment: Digest, storage_commitment: Digest, + anchor_block_hash: Digest, ) -> Result { - get_account_seed_single( + compute_account_seed_single( init_seed, account_type, storage_mode, + version, code_commitment, storage_commitment, + anchor_block_hash, ) } /// Finds and returns a seed suitable for creating an account ID for the specified account type /// using the provided initial seed as a starting point. Using a single thread. -pub fn get_account_seed_single( +#[cfg(not(feature = "concurrent"))] +pub fn compute_account_seed_single( init_seed: [u8; 32], account_type: AccountType, storage_mode: AccountStorageMode, + version: AccountIdVersion, code_commitment: Digest, storage_commitment: Digest, + anchor_block_hash: Digest, ) -> Result { let init_seed: Vec<[u8; 8]> = init_seed.chunks(8).map(|chunk| chunk.try_into().unwrap()).collect(); @@ -154,7 +171,8 @@ pub fn get_account_seed_single( Felt::new(u64::from_le_bytes(init_seed[2])), Felt::new(u64::from_le_bytes(init_seed[3])), ]; - let mut current_digest = compute_digest(current_seed, code_commitment, storage_commitment); + let mut current_digest = + compute_digest(current_seed, code_commitment, storage_commitment, anchor_block_hash); #[cfg(feature = "log")] let mut log = log::Log::start(current_digest, current_seed, account_type, storage_mode); @@ -165,20 +183,24 @@ pub fn get_account_seed_single( log.iteration(current_digest, current_seed); // check if the seed satisfies the specified account type - if AccountId::validate_seed_digest(¤t_digest).is_ok() { - if let Ok(account_id) = AccountId::try_from(current_digest[0]) { - if account_id.account_type() == account_type - && account_id.storage_mode() == storage_mode - { - #[cfg(feature = "log")] - log.done(current_digest, current_seed, account_id); - - return Ok(current_seed); - }; - } + let first_felt = current_digest.as_elements()[0]; + if let Ok((computed_account_type, computed_storage_mode, computed_version)) = + validate_first_felt(first_felt) + { + if computed_account_type == account_type + && computed_storage_mode == storage_mode + && computed_version == version + { + #[cfg(feature = "log")] + log.done(current_digest, current_seed); + + return Ok(current_seed); + }; } + current_seed = current_digest.into(); - current_digest = compute_digest(current_seed, code_commitment, storage_commitment); + current_digest = + compute_digest(current_seed, code_commitment, storage_commitment, anchor_block_hash); } } @@ -190,8 +212,8 @@ mod log { use miden_crypto::FieldElement; use super::{ - super::{account_id::digest_pow, Digest, Word}, - AccountId, AccountType, + super::{Digest, Word}, + AccountType, }; use crate::accounts::AccountStorageMode; @@ -221,8 +243,7 @@ mod log { storage_mode: AccountStorageMode, ) -> Self { log::info!( - "Generating new account seed [pow={}, digest={}, seed={} type={:?} onchain={:?}]", - digest_pow(digest), + "Generating new account seed [digest={}, seed={} type={:?} onchain={:?}]", digest_hex(digest), word_hex(seed), account_type, @@ -235,12 +256,8 @@ mod log { pub fn iteration(&mut self, digest: Digest, seed: Word) { self.count += 1; - let pow = digest_pow(digest); - if pow >= self.pow { - self.digest = digest; - self.seed = seed; - self.pow = pow; - } + self.digest = digest; + self.seed = seed; if self.count % 500_000 == 0 { log::debug!( @@ -253,14 +270,12 @@ mod log { } } - pub fn done(self, digest: Digest, seed: Word, account_id: AccountId) { + pub fn done(self, digest: Digest, seed: Word) { log::info!( - "Found account seed [pow={}, current_digest={}, current_seed={} type={:?} onchain={}]]", - digest_pow(digest), + "Found account seed [current_digest={}, current_seed={}, count={}]]", digest_hex(digest), word_hex(seed), - account_id.account_type(), - account_id.is_public(), + self.count, ); } } diff --git a/objects/src/assets/fungible.rs b/objects/src/assets/fungible.rs index 05f37ac7d..36dbe8e1a 100644 --- a/objects/src/assets/fungible.rs +++ b/objects/src/assets/fungible.rs @@ -1,15 +1,11 @@ use alloc::{boxed::Box, string::ToString}; use core::fmt; -use vm_core::{ - utils::{ByteReader, ByteWriter, Deserializable, Serializable}, - FieldElement, -}; +use vm_core::utils::{ByteReader, ByteWriter, Deserializable, Serializable}; use vm_processor::DeserializationError; -use super::{ - is_not_a_non_fungible_asset, AccountId, AccountType, Asset, AssetError, Felt, Word, ZERO, -}; +use super::{is_not_a_non_fungible_asset, AccountType, Asset, AssetError, Felt, Word, ZERO}; +use crate::accounts::AccountId; // FUNGIBLE ASSET // ================================================================================================ @@ -31,8 +27,8 @@ impl FungibleAsset { /// The serialized size of a [`FungibleAsset`] in bytes. /// - /// Currently an account id (felt) plus an amount (u64). - pub const SERIALIZED_SIZE: usize = Felt::ELEMENT_BYTES + core::mem::size_of::(); + /// Currently an account id (15 bytes) plus an amount (u64). + pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::(); // CONSTRUCTOR // -------------------------------------------------------------------------------------------- @@ -50,7 +46,7 @@ impl FungibleAsset { /// Creates a new [FungibleAsset] without checking its validity. pub(crate) fn new_unchecked(value: Word) -> FungibleAsset { FungibleAsset { - faucet_id: AccountId::new_unchecked(value[3]), + faucet_id: AccountId::new_unchecked([value[3], value[2]]), amount: value[0].as_int(), } } @@ -75,9 +71,7 @@ impl FungibleAsset { /// Returns the key which is used to store this asset in the account vault. pub fn vault_key(&self) -> Word { - let mut key = Word::default(); - key[3] = self.faucet_id.into(); - key + Self::vault_key_from_faucet(self.faucet_id) } // OPERATIONS @@ -144,13 +138,22 @@ impl FungibleAsset { Ok(self) } + + /// Returns the key which is used to store this asset in the account vault. + pub(super) fn vault_key_from_faucet(faucet_id: AccountId) -> Word { + let mut key = Word::default(); + key[2] = faucet_id.second_felt(); + key[3] = faucet_id.first_felt(); + key + } } impl From for Word { fn from(asset: FungibleAsset) -> Self { let mut result = Word::default(); result[0] = Felt::new(asset.amount); - result[3] = asset.faucet_id.into(); + result[2] = asset.faucet_id.second_felt(); + result[3] = asset.faucet_id.first_felt(); debug_assert!(is_not_a_non_fungible_asset(result)); result } @@ -166,10 +169,10 @@ impl TryFrom for FungibleAsset { type Error = AssetError; fn try_from(value: Word) -> Result { - if (value[1], value[2]) != (ZERO, ZERO) { - return Err(AssetError::FungibleAssetExpectedZeroes(value)); + if value[1] != ZERO { + return Err(AssetError::FungibleAssetExpectedZero(value)); } - let faucet_id = AccountId::try_from(value[3]) + let faucet_id = AccountId::try_from([value[3], value[2]]) .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?; let amount = value[0].as_int(); Self::new(faucet_id, amount) @@ -188,7 +191,7 @@ impl fmt::Display for FungibleAsset { impl Serializable for FungibleAsset { fn write_into(&self, target: &mut W) { // All assets should serialize their faucet ID at the first position to allow them to be - // easily distinguishable during deserialization. + // distinguishable during deserialization. target.write(self.faucet_id); target.write(self.amount); } @@ -201,18 +204,8 @@ impl Serializable for FungibleAsset { impl Deserializable for FungibleAsset { fn read_from(source: &mut R) -> Result { let faucet_id: AccountId = source.read()?; - FungibleAsset::deserialize_with_account_id(faucet_id, source) - } -} - -impl FungibleAsset { - /// Deserializes a [`FungibleAsset`] from an [`AccountId`] and the remaining data from the given - /// `source`. - pub(super) fn deserialize_with_account_id( - faucet_id: AccountId, - source: &mut R, - ) -> Result { let amount: u64 = source.read()?; + FungibleAsset::new(faucet_id, amount) .map_err(|err| DeserializationError::InvalidValue(err.to_string())) } @@ -224,10 +217,13 @@ impl FungibleAsset { #[cfg(test)] mod tests { use super::*; - use crate::accounts::account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, + use crate::{ + accounts::AccountId, + testing::account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, + }, }; #[test] @@ -250,8 +246,14 @@ mod tests { let account_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3).unwrap(); let asset = FungibleAsset::new(account_id, 50).unwrap(); let mut asset_bytes = asset.to_bytes(); + assert_eq!(asset_bytes.len(), asset.get_size_hint()); + assert_eq!(asset.get_size_hint(), FungibleAsset::SERIALIZED_SIZE); + + let non_fungible_faucet_id = + AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap(); + // Set invalid Faucet ID. - asset_bytes[0..8].copy_from_slice(&ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN.to_le_bytes()); + asset_bytes[0..15].copy_from_slice(&non_fungible_faucet_id.to_bytes()); let err = FungibleAsset::read_from_bytes(&asset_bytes).unwrap_err(); assert!(matches!(err, DeserializationError::InvalidValue(_))); } diff --git a/objects/src/assets/mod.rs b/objects/src/assets/mod.rs index 64162a06b..971fef50e 100644 --- a/objects/src/assets/mod.rs +++ b/objects/src/assets/mod.rs @@ -1,8 +1,12 @@ use super::{ - accounts::{AccountId, AccountType, ACCOUNT_ISFAUCET_MASK}, + accounts::AccountType, utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, AssetError, Felt, Hasher, Word, ZERO, }; +use crate::accounts::{ + account_id::{self}, + AccountIdPrefix, +}; mod fungible; pub use fungible::FungibleAsset; @@ -24,13 +28,18 @@ pub use vault::AssetVault; /// All assets are encoded using a single word (4 elements) such that it is easy to determine the /// type of an asset both inside and outside Miden VM. Specifically: /// -/// Element 1 will be: +/// Element 1 of the asset will be: /// - ZERO for a fungible asset. /// - non-ZERO for a non-fungible asset. /// -/// The 3rd most significant bit will be: -/// - 1 for a fungible asset. -/// - 0 for a non-fungible asset. +/// Element 3 of both asset types is an [`AccountIdPrefix`] or equivalently, the first felt of an +/// [`AccountId`](crate::accounts::AccountId), which can be used to distinguish assets +/// based on [`AccountIdPrefix::account_type`]. +/// +/// For element 3 of the vault keys of assets, the 6th least significant bit (referred to as the +/// "fungible bit" will be): +/// - `1` for a fungible asset. +/// - `0` for a non-fungible asset. /// /// The above properties guarantee that there can never be a collision between a fungible and a /// non-fungible asset. @@ -38,8 +47,13 @@ pub use vault::AssetVault; /// The methodology for constructing fungible and non-fungible assets is described below. /// /// # Fungible assets -/// The most significant element of a fungible asset is set to the ID of the faucet which issued -/// the asset. This guarantees the properties described above (the 3rd most significant bit is ONE). +/// +/// - A fungible asset's data layout is: `[amount, 0, faucet_id_lo, faucet_id_hi]`. +/// - A fungible asset's vault key layout is: `[0, 0, faucet_id_lo, faucet_id_hi]`. +/// +/// The most significant elements of a fungible asset are set to the first (`faucet_id_hi`) and +/// second felt (`faucet_id_lo`) of the ID of the faucet which issues the asset. This guarantees the +/// properties described above (the fungible bit is `1`). /// /// The least significant element is set to the amount of the asset. This amount cannot be greater /// than 2^63 - 1 and thus requires 63-bits to store. @@ -51,11 +65,17 @@ pub use vault::AssetVault; /// for each faucet as per the faucet creation logic. /// /// # Non-fungible assets +/// +/// - A non-fungible asset's data layout is: `[hash0, hash1, hash2, faucet_id_hi]`. +/// - A non-fungible asset's vault key layout is: `[faucet_id_hi, hash1, hash2, hash0']`, where +/// `hash0'` is equivalent to `hash0` with the fungible bit set to `0`. See +/// [`NonFungibleAsset::vault_key`] for more details. +/// /// The 4 elements of non-fungible assets are computed as follows: /// - First the asset data is hashed. This compresses an asset of an arbitrary length to 4 field -/// elements: [d0, d1, d2, d3]. -/// - d1 is then replaced with the faucet_id which issues the asset: [d0, faucet_id, d2, d3]. -/// - Lastly, the 3rd most significant bit of d3 is set to ZERO. +/// elements: `[hash0, hash1, hash2, hash3]`. +/// - `hash3` is then replaced with the first felt of the faucet ID (`faucet_id_hi`) which issues +/// the asset: `[hash0, hash1, hash2, faucet_id_hi]`. /// /// It is impossible to find a collision between two non-fungible assets issued by different faucets /// as the faucet_id is included in the description of the non-fungible asset and this is guaranteed @@ -96,10 +116,18 @@ impl Asset { matches!(self, Self::Fungible(_)) } - /// Returns ID of the faucet which issued this asset. - pub fn faucet_id(&self) -> AccountId { + /// Returns true if this asset is a non fungible asset. + pub const fn is_non_fungible(&self) -> bool { + matches!(self, Self::NonFungible(_)) + } + + /// Returns the prefix of the faucet ID which issued this asset. + /// + /// To get the full [`AccountId`](crate::accounts::AccountId ) of a fungible asset the asset + /// must be matched on. + pub fn faucet_id_prefix(&self) -> AccountIdPrefix { match self { - Self::Fungible(asset) => asset.faucet_id(), + Self::Fungible(asset) => asset.faucet_id().prefix(), Self::NonFungible(asset) => asset.faucet_id(), } } @@ -185,17 +213,18 @@ impl Serializable for Asset { impl Deserializable for Asset { fn read_from(source: &mut R) -> Result { - // Both asset types have their faucet ID as the first element, so we can use it to inspect - // what type of asset it is. - let account_id: AccountId = source.read()?; - let account_type = account_id.account_type(); + // Both asset types have their faucet ID prefix as the first element, so we can use it to + // inspect what type of asset it is. + // Due to little endian byte order, the first byte contains the account ID metadata. + let account_metadata = source.peek_u8()?; + let account_type = account_id::extract_type(account_metadata as u64); match account_type { AccountType::FungibleFaucet => { - FungibleAsset::deserialize_with_account_id(account_id, source).map(Asset::from) + FungibleAsset::read_from(source).map(Asset::from) }, AccountType::NonFungibleFaucet => { - NonFungibleAsset::deserialize_with_account_id(account_id, source).map(Asset::from) + NonFungibleAsset::read_from(source).map(Asset::from) }, other_type => { Err(DeserializationError::InvalidValue(format!( @@ -214,9 +243,17 @@ impl Deserializable for Asset { /// Note: this does not mean that the word is a fungible asset as the word may contain a value /// which is not a valid asset. fn is_not_a_non_fungible_asset(asset: Word) -> bool { - // For fungible assets, the position `3` contains the faucet's account id, in which case the - // bit is set. For non-fungible assets have the bit always set to `0`. - (asset[3].as_int() & ACCOUNT_ISFAUCET_MASK) == ACCOUNT_ISFAUCET_MASK + match AccountIdPrefix::try_from(asset[3]) { + Ok(prefix) => { + matches!(prefix.account_type(), AccountType::FungibleFaucet) + }, + Err(err) => { + #[cfg(debug_assertions)] + panic!("invalid account id prefix passed to is_not_a_non_fungible_asset: {err}"); + #[cfg(not(debug_assertions))] + false + }, + } } // TESTS @@ -231,14 +268,14 @@ mod tests { }; use super::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; - use crate::accounts::{ - account_id::testing::{ + use crate::{ + accounts::{account_id, AccountId}, + testing::account_id::{ ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, }, - AccountId, }; #[test] @@ -261,7 +298,7 @@ mod tests { ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, ] { let account_id = AccountId::try_from(non_fungible_account_id).unwrap(); - let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(); + let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap(); let non_fungible_asset: Asset = NonFungibleAsset::new(&details).unwrap().into(); assert_eq!( non_fungible_asset, @@ -290,9 +327,35 @@ mod tests { ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, ] { let account_id = AccountId::try_from(non_fungible_account_id).unwrap(); - let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(); + let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap(); let non_fungible_asset: Asset = NonFungibleAsset::new(&details).unwrap().into(); assert_eq!(non_fungible_asset, Asset::new_unchecked(Word::from(non_fungible_asset))); } } + + /// This test asserts that account ID's metadata is serialized in the first byte of assets. + /// Asset deserialization relies on that fact and if this changes the serialization must + /// be updated. + #[test] + fn test_account_id_metadata_is_in_first_serialized_byte() { + for asset in [FungibleAsset::mock(300), NonFungibleAsset::mock(&[0xaa, 0xbb])] { + let serialized_asset = asset.to_bytes(); + // Get the first byte and interpret it as a u64 because the extract functions require + // it. + let first_byte = serialized_asset[0] as u64; + + assert_eq!( + account_id::extract_type(first_byte), + asset.faucet_id_prefix().account_type() + ); + assert_eq!( + account_id::extract_storage_mode(first_byte).unwrap(), + asset.faucet_id_prefix().storage_mode() + ); + assert_eq!( + account_id::extract_version(first_byte).unwrap(), + asset.faucet_id_prefix().version() + ); + } + } } diff --git a/objects/src/assets/nonfungible.rs b/objects/src/assets/nonfungible.rs index f34f24ca7..5dc978386 100644 --- a/objects/src/assets/nonfungible.rs +++ b/objects/src/assets/nonfungible.rs @@ -3,27 +3,31 @@ use core::fmt; use vm_core::{FieldElement, WORD_SIZE}; -use super::{AccountId, AccountType, Asset, AssetError, Felt, Hasher, Word, ACCOUNT_ISFAUCET_MASK}; +use super::{AccountIdPrefix, AccountType, Asset, AssetError, Felt, Hasher, Word}; use crate::{ + accounts::AccountId, utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, Digest, }; -/// Position of the faucet_id inside the [NonFungibleAsset] word. -const FAUCET_ID_POS: usize = 1; +/// Position of the faucet_id inside the [`NonFungibleAsset`] word. +const FAUCET_ID_POS: usize = 3; // NON-FUNGIBLE ASSET // ================================================================================================ + /// A commitment to a non-fungible asset. /// /// The commitment is constructed as follows: /// -/// - Hash the asset data producing `[d0, d1, d2, d3]`. -/// - Replace the value of `d1` with the faucet id producing `[d0, faucet_id, d2, d3]`. -/// - Force the bit position [ACCOUNT_ISFAUCET_MASK] of `d3` to be `0`. +/// - Hash the asset data producing `[hash0, hash1, hash2, hash3]`. +/// - Replace the value of `hash3` with the first felt of the faucet id (`faucet_id_hi`) producing +/// `[hash0, hash1, hash2, faucet_id_hi]`. +/// - This layout ensures that fungible and non-fungible assets are distinguishable by interpreting +/// the 3rd element of an asset as an [`AccountIdPrefix`] and checking its type. /// -/// [NonFungibleAsset] itself does not contain the actual asset data. The container for this data -/// [NonFungibleAssetDetails] struct. +/// [`NonFungibleAsset`] itself does not contain the actual asset data. The container for this data +/// is [`NonFungibleAssetDetails`]. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct NonFungibleAsset(Word); @@ -68,35 +72,14 @@ impl NonFungibleAsset { /// /// # Errors /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet. - pub fn from_parts(faucet_id: AccountId, mut data_hash: Word) -> Result { + pub fn from_parts(faucet_id: AccountIdPrefix, mut data_hash: Word) -> Result { if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) { return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id)); } - data_hash[FAUCET_ID_POS] = faucet_id.into(); - - // Forces the bit at position `ACCOUNT_ISFAUCET_MASK` to `0`. - // - // Explanation of the bit flip: - // - // - assets require a faucet account, the id of such accounts always has the bit at the mask - // position. - // - fungible assets have the account id at position `3`, meaning the 3rd bit is always of - // the element at the 3rd position is always 1. - // - non-fungible assets, have the account id at position `FAUCET_ID_POS`, so the bit at - // position `3` can be used to identify fungible vs. non-fungible assets - // - // This is done as an optimization, since the field element at position `3` is used as index - // when storing the assets into the asset vault. This strategy forces fungible assets to be - // assigned to the same slot because it uses the faucet's account id, and allows for easy - // merging of fungible faucets. At the same time, it spreads the non-fungible assets evenly - // across the vault, because in this case the element is the result of a cryptographic hash - // function. - let d3 = data_hash[3].as_int(); - data_hash[3] = Felt::new((d3 & ACCOUNT_ISFAUCET_MASK) ^ d3); - - let asset = Self(data_hash); - Ok(asset) + data_hash[FAUCET_ID_POS] = Felt::from(faucet_id); + + Ok(Self(data_hash)) } /// Creates a new [NonFungibleAsset] without checking its validity. @@ -110,13 +93,40 @@ impl NonFungibleAsset { // ACCESSORS // -------------------------------------------------------------------------------------------- + + /// Returns the vault key of the [`NonFungibleAsset`]. + /// + /// This is the same as the asset with the following modifications, in this order: + /// - Swaps the faucet ID at index 0 and `hash0` at index 3. + /// - Sets the fungible bit for `hash0` to `0`. + /// + /// # Rationale + /// + /// This means `hash0` will be used as the leaf index in the asset SMT which ensures that a + /// non-fungible faucet's assets generally end up in different leaves as the key is not based on + /// the faucet ID. + /// + /// It also ensures that there is never any collision in the leaf index between a non-fungible + /// asset and a fungible asset, as the former's vault key always has the fungible bit set to `0` + /// and the latter's vault key always has the bit set to `1`. pub fn vault_key(&self) -> Word { - self.0 + let mut vault_key = self.0; + + // Swap first felt of faucet ID with hash0. + vault_key.swap(0, 3); + + // Set the fungible bit to zero by taking the bitwise `and` of the felt with the inverted + // is_faucet mask. + let clear_fungible_bit_mask = !AccountId::IS_FAUCET_MASK; + vault_key[3] = Felt::try_from(vault_key[3].as_int() & clear_fungible_bit_mask) + .expect("felt should still be valid as we cleared a bit and did not set any"); + + vault_key } /// Return ID of the faucet which issued this asset. - pub fn faucet_id(&self) -> AccountId { - AccountId::new_unchecked(self.0[FAUCET_ID_POS]) + pub fn faucet_id(&self) -> AccountIdPrefix { + AccountIdPrefix::new_unchecked(self.0[FAUCET_ID_POS]) } // HELPER FUNCTIONS @@ -128,7 +138,7 @@ impl NonFungibleAsset { /// - The faucet_id is not a valid non-fungible faucet ID. /// - The most significant bit of the asset is not ZERO. fn validate(&self) -> Result<(), AssetError> { - let faucet_id = AccountId::try_from(self.0[FAUCET_ID_POS]) + let faucet_id = AccountIdPrefix::try_from(self.0[FAUCET_ID_POS]) .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?; let account_type = faucet_id.account_type(); @@ -176,9 +186,9 @@ impl Serializable for NonFungibleAsset { // All assets should serialize their faucet ID at the first position to allow them to be // easily distinguishable during deserialization. target.write(self.0[FAUCET_ID_POS]); - target.write(self.0[0]); target.write(self.0[2]); - target.write(self.0[3]); + target.write(self.0[1]); + target.write(self.0[0]); } fn get_size_hint(&self) -> usize { @@ -188,27 +198,15 @@ impl Serializable for NonFungibleAsset { impl Deserializable for NonFungibleAsset { fn read_from(source: &mut R) -> Result { - let faucet_id: AccountId = source.read()?; - - Self::deserialize_with_account_id(faucet_id, source) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) - } -} + let faucet_id_prefix: AccountIdPrefix = source.read()?; -impl NonFungibleAsset { - /// Deserializes a [`NonFungibleAsset`] from an [`AccountId`] and the remaining data from the - /// given `source`. - pub(super) fn deserialize_with_account_id( - faucet_id: AccountId, - source: &mut R, - ) -> Result { - let hash_0: Felt = source.read()?; let hash_2: Felt = source.read()?; - let hash_3: Felt = source.read()?; + let hash_1: Felt = source.read()?; + let hash_0: Felt = source.read()?; // The second felt in the data_hash will be replaced by the faucet id, so we can set it to // zero here. - NonFungibleAsset::from_parts(faucet_id, [hash_0, Felt::ZERO, hash_2, hash_3]) + NonFungibleAsset::from_parts(faucet_id_prefix, [hash_0, hash_1, hash_2, Felt::ZERO]) .map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } @@ -221,7 +219,7 @@ impl NonFungibleAsset { /// Unlike [NonFungibleAsset] struct, this struct contains full details of a non-fungible asset. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NonFungibleAssetDetails { - faucet_id: AccountId, + faucet_id: AccountIdPrefix, asset_data: Vec, } @@ -230,7 +228,7 @@ impl NonFungibleAssetDetails { /// /// # Errors /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet. - pub fn new(faucet_id: AccountId, asset_data: Vec) -> Result { + pub fn new(faucet_id: AccountIdPrefix, asset_data: Vec) -> Result { if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) { return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id)); } @@ -239,7 +237,7 @@ impl NonFungibleAssetDetails { } /// Returns ID of the faucet which issued this asset. - pub fn faucet_id(&self) -> AccountId { + pub fn faucet_id(&self) -> AccountIdPrefix { self.faucet_id } @@ -254,10 +252,15 @@ impl NonFungibleAssetDetails { #[cfg(test)] mod tests { + use assert_matches::assert_matches; + use super::*; - use crate::accounts::account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, - ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, + use crate::{ + accounts::AccountId, + testing::account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, + ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, + }, }; #[test] @@ -268,7 +271,7 @@ mod tests { ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, ] { let account_id = AccountId::try_from(non_fungible_account_id).unwrap(); - let details = NonFungibleAssetDetails::new(account_id, vec![1, 2, 3]).unwrap(); + let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap(); let non_fungible_asset = NonFungibleAsset::new(&details).unwrap(); assert_eq!( non_fungible_asset, @@ -277,12 +280,16 @@ mod tests { } let account = AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap(); - let details = NonFungibleAssetDetails::new(account, vec![4, 5, 6, 7]).unwrap(); + let details = NonFungibleAssetDetails::new(account.prefix(), vec![4, 5, 6, 7]).unwrap(); let asset = NonFungibleAsset::new(&details).unwrap(); let mut asset_bytes = asset.to_bytes(); - // Set invalid Faucet ID. - asset_bytes[0..8].copy_from_slice(&ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN.to_le_bytes()); + + let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap(); + + // Set invalid Faucet ID Prefix. + asset_bytes[0..8].copy_from_slice(&fungible_faucet_id.prefix().to_bytes()); + let err = NonFungibleAsset::read_from_bytes(&asset_bytes).unwrap_err(); - assert!(matches!(err, DeserializationError::InvalidValue(_))); + assert_matches!(err, DeserializationError::InvalidValue(msg) if msg.contains("must be of type NonFungibleFaucet")); } } diff --git a/objects/src/assets/vault.rs b/objects/src/assets/vault.rs index 402edbc6e..d1f4f08a1 100644 --- a/objects/src/assets/vault.rs +++ b/objects/src/assets/vault.rs @@ -1,11 +1,11 @@ use alloc::{string::ToString, vec::Vec}; use super::{ - AccountId, AccountType, Asset, ByteReader, ByteWriter, Deserializable, DeserializationError, - FungibleAsset, NonFungibleAsset, Serializable, ZERO, + AccountType, Asset, ByteReader, ByteWriter, Deserializable, DeserializationError, + FungibleAsset, NonFungibleAsset, Serializable, }; use crate::{ - accounts::{AccountVaultDelta, NonFungibleDeltaAction}, + accounts::{AccountId, AccountVaultDelta, NonFungibleDeltaAction}, crypto::merkle::Smt, AssetVaultError, Digest, }; @@ -69,7 +69,10 @@ impl AssetVault { } // if the tree value is [0, 0, 0, 0], the asset is not stored in the vault - match self.asset_tree.get_value(&[ZERO, ZERO, ZERO, faucet_id.into()].into()) { + match self + .asset_tree + .get_value(&FungibleAsset::vault_key_from_faucet(faucet_id).into()) + { asset if asset == Smt::EMPTY_VALUE => Ok(0), asset => Ok(FungibleAsset::new_unchecked(asset).amount()), } diff --git a/objects/src/block/header.rs b/objects/src/block/header.rs index 33525999d..5e91651e3 100644 --- a/objects/src/block/header.rs +++ b/objects/src/block/header.rs @@ -43,6 +43,13 @@ pub struct BlockHeader { } impl BlockHeader { + /// The length of an epoch expressed as a power of two. `2^(EPOCH_LENGTH_EXPONENT)` is the + /// number of blocks in an epoch. + /// + /// The epoch of a block can be obtained by shifting the block number to the right by this + /// exponent. + pub const EPOCH_LENGTH_EXPONENT: u8 = 16; + /// Creates a new block header. #[allow(clippy::too_many_arguments)] pub fn new( @@ -126,6 +133,13 @@ impl BlockHeader { self.block_num } + /// Returns the epoch to which this block belongs. + /// + /// This is the block number shifted right by [`Self::EPOCH_LENGTH_EXPONENT`]. + pub fn block_epoch(&self) -> u16 { + block_epoch_from_number(self.block_num) + } + /// Returns the chain root. pub fn chain_root(&self) -> Digest { self.chain_root @@ -207,6 +221,9 @@ impl BlockHeader { } } +// SERIALIZATION +// ================================================================================================ + impl Serializable for BlockHeader { fn write_into(&self, target: &mut W) { self.version.write_into(target); @@ -253,6 +270,19 @@ impl Deserializable for BlockHeader { } } +// UTILITIES +// ================================================================================================ + +/// Returns the block number of the epoch block for the given `epoch`. +pub const fn block_num_from_epoch(epoch: u16) -> u32 { + (epoch as u32) << BlockHeader::EPOCH_LENGTH_EXPONENT +} + +/// Returns the epoch of the given block number. +pub const fn block_epoch_from_number(block_number: u32) -> u16 { + (block_number >> BlockHeader::EPOCH_LENGTH_EXPONENT) as u16 +} + #[cfg(test)] mod tests { use vm_core::Word; diff --git a/objects/src/block/mod.rs b/objects/src/block/mod.rs index ac93ecdc1..d915dea8a 100644 --- a/objects/src/block/mod.rs +++ b/objects/src/block/mod.rs @@ -6,7 +6,7 @@ use super::{ }; mod header; -pub use header::BlockHeader; +pub use header::{block_epoch_from_number, block_num_from_epoch, BlockHeader}; mod note_tree; pub use note_tree::{BlockNoteIndex, BlockNoteTree}; @@ -229,7 +229,8 @@ pub fn compute_tx_hash( ) -> Digest { let mut elements = vec![]; for (transaction_id, account_id) in updated_accounts { - elements.extend_from_slice(&[account_id.into(), ZERO, ZERO, ZERO]); + let account_id_felts: [Felt; 2] = account_id.into(); + elements.extend_from_slice(&[account_id_felts[0], account_id_felts[1], ZERO, ZERO]); elements.extend_from_slice(transaction_id.as_elements()); } diff --git a/objects/src/errors.rs b/objects/src/errors.rs index f94a78f51..fb3055f88 100644 --- a/objects/src/errors.rs +++ b/objects/src/errors.rs @@ -4,7 +4,7 @@ use core::error::Error; use assembly::{diagnostics::reporting::PrintDiagnostic, Report}; use miden_crypto::utils::HexParseError; use thiserror::Error; -use vm_core::Felt; +use vm_core::{Felt, FieldElement}; use vm_processor::DeserializationError; use super::{ @@ -16,7 +16,8 @@ use super::{ MAX_OUTPUT_NOTES_PER_BATCH, MAX_OUTPUT_NOTES_PER_BLOCK, }; use crate::{ - accounts::{AccountCode, AccountStorage, AccountType}, + accounts::{AccountCode, AccountIdPrefix, AccountStorage, AccountType}, + block::block_num_from_epoch, notes::{NoteAssets, NoteExecutionHint, NoteTag, NoteType, Nullifier}, ACCOUNT_UPDATE_MAX_SIZE, MAX_INPUTS_PER_NOTE, MAX_INPUT_NOTES_PER_TX, MAX_OUTPUT_NOTES_PER_TX, }; @@ -45,8 +46,6 @@ pub enum AccountError { AccountCodeProcedureInvalidPadding(Digest), #[error("failed to convert bytes into account id field element")] AccountIdInvalidFieldElement(#[source] DeserializationError), - #[error("account id contains {0} 1s but must contain at least {min} 1s", min = AccountId::MIN_ACCOUNT_ONES)] - AccountIdTooFewOnes(u32), #[error("failed to update asset vault")] AssetVaultUpdateError(#[source] AssetVaultError), #[error("account build error: {0}")] @@ -137,8 +136,10 @@ pub enum AssetError { FungibleAssetAmountTooBig(u64), #[error("subtracting {subtrahend} from fungible asset amount {minuend} would overflow")] FungibleAssetAmountNotSufficient { minuend: u64, subtrahend: u64 }, - #[error("fungible asset word {0:?} does not contain expected ZEROs at word index 1 and 2")] - FungibleAssetExpectedZeroes(Word), + #[error("fungible asset word {hex} does not contain expected ZERO at word index 1", + hex = vm_core::utils::to_hex(Felt::elements_as_bytes(.0)) + )] + FungibleAssetExpectedZero(Word), #[error("cannot add fungible asset with issuer {other_issuer} to fungible asset with issuer {original_issuer}")] FungibleAssetInconsistentFaucetIds { original_issuer: AccountId, @@ -157,7 +158,7 @@ pub enum AssetError { id_type = .0.account_type(), expected_ty = AccountType::NonFungibleFaucet )] - NonFungibleFaucetIdTypeMismatch(AccountId), + NonFungibleFaucetIdTypeMismatch(AccountIdPrefix), #[error("{0}")] TokenSymbolError(String), } @@ -207,6 +208,8 @@ pub enum NoteError { to = NoteExecutionHint::ON_BLOCK_SLOT_TAG, )] NoteExecutionHintTagOutOfRange(u8), + #[error("note execution hint after block variant cannot contain u32::MAX")] + NoteExecutionHintAfterBlockCannotBeU32Max, #[error("invalid note execution hint payload {1} for tag {0}")] InvalidNoteExecutionHintPayload(u8, u32), #[error("note type {0:b} does not match any of the valid note types {public}, {private} or {encrypted}", @@ -281,6 +284,11 @@ pub enum TransactionInputError { AccountSeedNotProvidedForNewAccount, #[error("account seed must not be provided for existing accounts")] AccountSeedProvidedForExistingAccount, + #[error( + "anchor block header for epoch {0} (block number = {block_number}) must be provided in the chain mmr for the new account", + block_number = block_num_from_epoch(*.0), + )] + AnchorBlockHeaderNotProvidedForNewAccount(u16), #[error("transaction input note with nullifier {0} is a duplicate")] DuplicateInputNote(Nullifier), #[error("ID {expected} of the new account does not match the ID {actual} computed from the provided seed")] diff --git a/objects/src/notes/assets.rs b/objects/src/notes/assets.rs index fee03f274..4c642fc5e 100644 --- a/objects/src/notes/assets.rs +++ b/objects/src/notes/assets.rs @@ -221,14 +221,15 @@ impl Deserializable for NoteAssets { mod tests { use super::{compute_asset_commitment, NoteAssets}; use crate::{ - accounts::account_id::{testing::ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, AccountId}, + accounts::AccountId, assets::{Asset, FungibleAsset}, - Digest, Felt, + testing::account_id::ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, + Digest, }; #[test] fn add_asset() { - let faucet_id = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN)); + let faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap(); let asset1 = Asset::Fungible(FungibleAsset::new(faucet_id, 100).unwrap()); let asset2 = Asset::Fungible(FungibleAsset::new(faucet_id, 50).unwrap()); diff --git a/objects/src/notes/execution_hint.rs b/objects/src/notes/execution_hint.rs index 3aa7b0552..44fbcea87 100644 --- a/objects/src/notes/execution_hint.rs +++ b/objects/src/notes/execution_hint.rs @@ -10,6 +10,16 @@ use crate::NoteError; /// /// This struct can be represented as the combination of a tag, and a payload. /// The tag specifies the variant of the hint, and the payload encodes the hint data. +/// +/// # Felt layout +/// +/// [`NoteExecutionHint`] can be encoded into a [`Felt`] with the following layout: +/// +/// ```text +/// [26 zero bits | payload (32 bits) | tag (6 bits)] +/// ``` +/// +/// This way, hints such as [NoteExecutionHint::Always], are represented by `Felt::new(1)`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum NoteExecutionHint { /// Unspecified note execution hint. Implies it is not known under which conditions the note @@ -17,8 +27,11 @@ pub enum NoteExecutionHint { None, /// The note's script can be executed at any time. Always, - /// The note's script can be executed after the specified block height. - AfterBlock { block_num: u32 }, + /// The note's script can be executed after the specified block number. + /// + /// The block number cannot be [`u32::MAX`] which is enforced through the [`AfterBlockNumber`] + /// type. + AfterBlock { block_num: AfterBlockNumber }, /// The note's script can be executed in the specified slot within the specified epoch. /// /// The slot is defined as follows: @@ -62,8 +75,13 @@ impl NoteExecutionHint { } /// Creates a [NoteExecutionHint::AfterBlock] variant based on the given `block_num` - pub fn after_block(block_num: u32) -> Self { - NoteExecutionHint::AfterBlock { block_num } + /// + /// # Errors + /// + /// Returns an error if `block_num` is equal to [`u32::MAX`]. + pub fn after_block(block_num: u32) -> Result { + AfterBlockNumber::new(block_num) + .map(|block_number| NoteExecutionHint::AfterBlock { block_num: block_number }) } /// Creates a [NoteExecutionHint::OnBlockSlot] for the given parameters @@ -85,7 +103,7 @@ impl NoteExecutionHint { } Ok(NoteExecutionHint::Always) }, - Self::AFTER_BLOCK_TAG => Ok(NoteExecutionHint::AfterBlock { block_num: payload }), + Self::AFTER_BLOCK_TAG => NoteExecutionHint::after_block(payload), Self::ON_BLOCK_SLOT_TAG => { let remainder = (payload >> 24 & 0xff) as u8; if remainder != 0 { @@ -114,7 +132,7 @@ impl NoteExecutionHint { NoteExecutionHint::None => None, NoteExecutionHint::Always => Some(true), NoteExecutionHint::AfterBlock { block_num: hint_block_num } => { - Some(block_num >= *hint_block_num) + Some(block_num >= hint_block_num.as_u32()) }, NoteExecutionHint::OnBlockSlot { epoch_len, slot_len, slot_offset } => { let epoch_len_blocks: u32 = 1 << epoch_len; @@ -132,11 +150,22 @@ impl NoteExecutionHint { } } + /// Encodes the [`NoteExecutionHint`] into a 6-bit tag and a 32-bit payload. + /// + /// # Guarantees + /// + /// Since the tag has at most 6 bits, the returned byte is guaranteed to have its two most + /// significant bits set to `0`. + /// + /// The payload is guaranteed to contain at least one `0` bit to make encoding it into + /// [`NoteMetadata`](crate::notes::NoteMetadata) safely possible. pub fn into_parts(&self) -> (u8, u32) { match self { NoteExecutionHint::None => (Self::NONE_TAG, 0), NoteExecutionHint::Always => (Self::ALWAYS_TAG, 0), - NoteExecutionHint::AfterBlock { block_num } => (Self::AFTER_BLOCK_TAG, *block_num), + NoteExecutionHint::AfterBlock { block_num } => { + (Self::AFTER_BLOCK_TAG, block_num.as_u32()) + }, NoteExecutionHint::OnBlockSlot { epoch_len, slot_len, slot_offset } => { let payload: u32 = ((*epoch_len as u32) << 16) | ((*slot_len as u32) << 8) | (*slot_offset as u32); @@ -146,12 +175,7 @@ impl NoteExecutionHint { } } -/// As a Felt, the ExecutionHint is encoded as: -/// -/// - 6 least significant bits: Hint identifier (tag). -/// - Bits 6 to 38: Hint payload. -/// -/// This way, hints such as [NoteExecutionHint::Always], are represented by `Felt::new(1)` +/// Converts a [`NoteExecutionHint`] into a [`Felt`] with the layout documented on the type. impl From for Felt { fn from(value: NoteExecutionHint) -> Self { let int_representation: u64 = value.into(); @@ -159,12 +183,10 @@ impl From for Felt { } } -/// As a u64, the ExecutionHint is encoded as: +/// Tries to convert a `u64` into a [`NoteExecutionHint`] with the expected layout documented on the +/// type. /// -/// - 6 least significant bits: Hint identifier (tag). -/// - Bits 6 to 38: Hint payload. -/// -/// This way, hints such as [NoteExecutionHint::Always], are represented by `1u64` +/// Note: The upper 26 bits are not enforced to be zero. impl TryFrom for NoteExecutionHint { type Error = NoteError; fn try_from(value: u64) -> Result { @@ -175,6 +197,7 @@ impl TryFrom for NoteExecutionHint { } } +/// Converts a [`NoteExecutionHint`] into a `u64` with the layout documented on the type. impl From for u64 { fn from(value: NoteExecutionHint) -> Self { let (tag, payload) = value.into_parts(); @@ -182,11 +205,57 @@ impl From for u64 { } } +// AFTER BLOCK NUMBER +// ================================================================================================ + +/// A wrapper around a block number which enforces that it is not `u32::MAX`. +/// +/// Used for the [`NoteExecutionHint::AfterBlock`] variant where this constraint is needed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AfterBlockNumber(u32); + +impl AfterBlockNumber { + /// Creates a new [`AfterBlockNumber`] from the given `block_number`. + /// + /// # Errors + /// + /// Returns an error if: + /// - `block_number` is equal to `u32::MAX`. + pub fn new(block_number: u32) -> Result { + if block_number == u32::MAX { + Err(NoteError::NoteExecutionHintAfterBlockCannotBeU32Max) + } else { + Ok(Self(block_number)) + } + } + + /// Returns the block number as a `u32`. + pub fn as_u32(&self) -> u32 { + self.0 + } +} + +impl From for u32 { + fn from(block_number: AfterBlockNumber) -> Self { + block_number.0 + } +} + +impl TryFrom for AfterBlockNumber { + type Error = NoteError; + + fn try_from(block_number: u32) -> Result { + Self::new(block_number) + } +} + // TESTS // ================================================================================================ #[cfg(test)] mod tests { + use assert_matches::assert_matches; + use super::*; fn assert_hint_serde(note_execution_hint: NoteExecutionHint) { @@ -199,7 +268,7 @@ mod tests { fn test_serialization_round_trip() { assert_hint_serde(NoteExecutionHint::None); assert_hint_serde(NoteExecutionHint::Always); - assert_hint_serde(NoteExecutionHint::AfterBlock { block_num: 15 }); + assert_hint_serde(NoteExecutionHint::after_block(15).unwrap()); assert_hint_serde(NoteExecutionHint::OnBlockSlot { epoch_len: 9, slot_len: 12, @@ -209,7 +278,7 @@ mod tests { #[test] fn test_encode_round_trip() { - let hint = NoteExecutionHint::AfterBlock { block_num: 15 }; + let hint = NoteExecutionHint::after_block(15).unwrap(); let hint_int: u64 = hint.into(); let decoded_hint: NoteExecutionHint = hint_int.try_into().unwrap(); assert_eq!(hint, decoded_hint); @@ -235,7 +304,7 @@ mod tests { let always = NoteExecutionHint::always(); assert!(always.can_be_consumed(100).unwrap()); - let after_block = NoteExecutionHint::after_block(12345); + let after_block = NoteExecutionHint::after_block(12345).unwrap(); assert!(!after_block.can_be_consumed(12344).unwrap()); assert!(after_block.can_be_consumed(12345).unwrap()); @@ -261,4 +330,12 @@ mod tests { NoteExecutionHint::from_parts(10, 1).unwrap_err(); } + + #[test] + fn test_after_block_fails_on_u32_max() { + assert_matches!( + NoteExecutionHint::after_block(u32::MAX).unwrap_err(), + NoteError::NoteExecutionHintAfterBlockCannotBeU32Max + ); + } } diff --git a/objects/src/notes/file.rs b/objects/src/notes/file.rs index a7d933d80..9c33fedc8 100644 --- a/objects/src/notes/file.rs +++ b/objects/src/notes/file.rs @@ -106,29 +106,26 @@ mod tests { }; use crate::{ - accounts::{ - account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, - ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, - }, - AccountId, - }, + accounts::AccountId, assets::{Asset, FungibleAsset}, notes::{ Note, NoteAssets, NoteFile, NoteInclusionProof, NoteInputs, NoteMetadata, NoteRecipient, NoteScript, NoteTag, NoteType, }, + testing::account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + }, }; fn create_example_note() -> Note { - let faucet = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN)); - let target = AccountId::new_unchecked(Felt::new( - ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, - )); + let faucet = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + let target = + AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN).unwrap(); let serial_num = [Felt::new(0), Felt::new(1), Felt::new(2), Felt::new(3)]; let script = NoteScript::mock(); - let note_inputs = NoteInputs::new(vec![target.into()]).unwrap(); + let note_inputs = NoteInputs::new(vec![target.prefix().into()]).unwrap(); let recipient = NoteRecipient::new(serial_num, script, note_inputs); let asset = Asset::Fungible(FungibleAsset::new(faucet, 100).unwrap()); diff --git a/objects/src/notes/metadata.rs b/objects/src/notes/metadata.rs index 2a5cd2959..f84fee277 100644 --- a/objects/src/notes/metadata.rs +++ b/objects/src/notes/metadata.rs @@ -15,6 +15,27 @@ use super::{ /// - For off-chain notes, the most significant bit of the tag must be 0. /// - For public notes, the second most significant bit of the tag must be 0. /// - For encrypted notes, two most significant bits of the tag must be 00. +/// +/// # Word layout & validity +/// +/// [`NoteMetadata`] can be encoded into a [`Word`] with the following layout: +/// +/// ```text +/// 1st felt: [sender_id_hi (64 bits)] +/// 2nd felt: [sender_id_lo (56 bits) | note_type (2 bits) | note_execution_hint_tag (6 bits)] +/// 3rd felt: [note_execution_hint_payload (32 bits) | note_tag (32 bits)] +/// 4th felt: [aux (64 bits)] +/// ``` +/// +/// The rationale for the above layout is to ensure the validity of each felt: +/// - 1st felt: Is equivalent to the first felt of the account ID so it inherits its validity. +/// - 2nd felt: The second felt of the account ID is designed such that its lower 8 bits can all be +/// set to `1` and still retain its validity due to the anchor epoch in the upper 16 bits always +/// containing at least one `0` bit. +/// - 3rd felt: The note execution hint payload must contain at least one `0` bit in its encoding, +/// so the upper 32 bits of the felt will contain at least one `0` bit making the entire felt +/// valid. +/// - 4th felt: The `aux` value must be a felt itself. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct NoteMetadata { /// The ID of the account which created the note. @@ -87,17 +108,27 @@ impl NoteMetadata { } impl From for Word { + /// Convert a [`NoteMetadata`] into a [`Word`]. + /// + /// The produced layout of the word is documented on the [`NoteMetadata`] type. fn from(metadata: NoteMetadata) -> Self { (&metadata).into() } } impl From<&NoteMetadata> for Word { + /// Convert a [`NoteMetadata`] into a [`Word`]. + /// + /// The produced layout of the word is documented on the [`NoteMetadata`] type. fn from(metadata: &NoteMetadata) -> Self { let mut elements = Word::default(); - elements[0] = metadata.tag.inner().into(); - elements[1] = metadata.sender.into(); - elements[2] = Felt::new(merge_type_and_hint(metadata.note_type, metadata.execution_hint)); + elements[0] = metadata.sender.first_felt(); + elements[1] = merge_id_type_and_hint_tag( + metadata.sender.second_felt(), + metadata.note_type, + metadata.execution_hint, + ); + elements[2] = merge_note_tag_and_hint_payload(metadata.execution_hint, metadata.tag); elements[3] = metadata.aux; elements } @@ -106,14 +137,22 @@ impl From<&NoteMetadata> for Word { impl TryFrom for NoteMetadata { type Error = NoteError; + /// Tries to decode a [`Word`] into a [`NoteMetadata`]. + /// + /// The expected layout of the word is documented on the [`NoteMetadata`] type. fn try_from(elements: Word) -> Result { - let sender = elements[1].try_into().map_err(NoteError::NoteSenderInvalidAccountId)?; - let (note_type, note_execution_hint) = unmerge_type_and_hint(elements[2].into())?; - let tag: u64 = elements[0].into(); - let tag: u32 = - tag.try_into().map_err(|_| NoteError::InconsistentNoteTag(note_type, tag))?; + let sender_id_first_felt: Felt = elements[0]; + + let (sender_id_second_felt, note_type, execution_hint_tag) = + unmerge_id_type_and_hint_tag(elements[1])?; - Self::new(sender, note_type, tag.into(), note_execution_hint, elements[3]) + let sender = AccountId::try_from([sender_id_first_felt, sender_id_second_felt]) + .map_err(NoteError::NoteSenderInvalidAccountId)?; + + let (execution_hint, note_tag) = + unmerge_note_tag_and_hint_payload(elements[2], execution_hint_tag)?; + + Self::new(sender, note_type, note_tag, execution_hint, elements[3]) } } @@ -122,54 +161,127 @@ impl TryFrom for NoteMetadata { impl Serializable for NoteMetadata { fn write_into(&self, target: &mut W) { - self.sender.write_into(target); - target.write_u64(merge_type_and_hint(self.note_type, self.execution_hint)); - self.tag.write_into(target); - self.aux.write_into(target); + // TODO: Do we need a serialization format that is different from the Word encoding? It was + // previously different. + Word::from(self).write_into(target); } } impl Deserializable for NoteMetadata { fn read_from(source: &mut R) -> Result { - let sender = AccountId::read_from(source)?; - let (note_type, note_execution_hint) = unmerge_type_and_hint(source.read_u64()?) - .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; - let tag = NoteTag::read_from(source)?; - let aux = Felt::read_from(source)?; - - Self::new(sender, note_type, tag, note_execution_hint, aux) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + let word = Word::read_from(source)?; + Self::try_from(word).map_err(|err| DeserializationError::InvalidValue(err.to_string())) } } // HELPER FUNCTIONS // ================================================================================================ -/// Encodes `note_type` and `note_execution_hint` into a [u64] such that the resulting number has -/// the following structure (from most significant bit to the least significant bit): +/// Merges the second felt of an [`AccountId`], a [`NoteType`] and the tag of a +/// [`NoteExecutionHint`] into a single [`Felt`]. +/// +/// The layout is as follows: +/// +/// ```text +/// [sender_id_lo (56 bits) | note_type (2 bits) | note_execution_hint_tag (6 bits)] +/// ``` /// -/// - Bits 39 to 38 (2 bits): NoteType -/// - Bits 37 to 6 (32 bits): NoteExecutionHint payload -/// - Bits 5 to 0 (6 bits): NoteExecutionHint tag -fn merge_type_and_hint(note_type: NoteType, note_execution_hint: NoteExecutionHint) -> u64 { - let type_nibble = note_type as u64 & 0b11; - let (tag_nibble, payload_u32) = note_execution_hint.into_parts(); +/// One of the upper 16 bits is guaranteed to be zero due to the guarantees of the epoch in the +/// account id. +/// +/// Note that `sender_id_lo` is the second felt of the sender's account ID. +fn merge_id_type_and_hint_tag( + sender_id_second_felt: Felt, + note_type: NoteType, + note_execution_hint: NoteExecutionHint, +) -> Felt { + let mut merged = sender_id_second_felt.as_int(); + + let type_bits = note_type as u8; + let (tag_bits, _) = note_execution_hint.into_parts(); + + debug_assert!(type_bits & 0b1111_1100 == 0, "note type must not contain values >= 4"); + debug_assert!( + tag_bits & 0b1100_0000 == 0, + "note execution hint tag must not contain values >= 64" + ); + + // Note: The least significant byte of the second AccountId felt is zero by construction so we + // can overwrite it. + merged |= (type_bits << 6) as u64; + merged |= tag_bits as u64; + + // SAFETY: One of the top 16 bits of the second felt (the anchor epoch) is zero by construction + // so the bytes will be a valid felt. + Felt::try_from(merged).expect("encoded value should be a valid felt") +} + +/// Unmerges the given felt into the second felt of an [`AccountId`], a [`NoteType`] and the tag of +/// a [`NoteExecutionHint`]. +fn unmerge_id_type_and_hint_tag(element: Felt) -> Result<(Felt, NoteType, u8), NoteError> { + let element = element.as_int(); - let payload_section = payload_u32 as u64; - let tag_section = (tag_nibble as u64) & 0b111111; + // Cut off the least significant byte. + let least_significant_byte = element as u8; + let note_type_bits = (least_significant_byte & 0b1100_0000) >> 6; + let tag_bits = least_significant_byte & 0b0011_1111; - (type_nibble << 38) | (payload_section << 6) | tag_section + let note_type = NoteType::try_from(note_type_bits)?; + + // Set least significant byte to zero. + let element = element & 0xffff_ffff_ffff_ff00; + + // SAFETY: The input was a valid felt and and we cleared additional bits and did not set any + // bits, so it must still be a valid felt. + let sender_id_second_felt = Felt::try_from(element).expect("element should still be valid"); + + Ok((sender_id_second_felt, note_type, tag_bits)) +} + +/// Merges the [`NoteExecutionHint`] payload and a [`NoteTag`] into a single [`Felt`]. +/// +/// The layout is as follows: +/// +/// ```text +/// [note_execution_hint_payload (32 bits) | note_tag (32 bits)] +/// ``` +/// +/// One of the upper 32 bits is guaranteed to be zero. +fn merge_note_tag_and_hint_payload( + note_execution_hint: NoteExecutionHint, + note_tag: NoteTag, +) -> Felt { + let (_, payload) = note_execution_hint.into_parts(); + let note_tag: u32 = note_tag.into(); + + debug_assert_ne!( + payload, + u32::MAX, + "payload should never be u32::MAX as it would produce an invalid felt" + ); + + let felt_int = ((payload as u64) << 32) | (note_tag as u64); + + // SAFETY: The payload is guaranteed to never be u32::MAX so at least one of the upper 32 bits + // is zero, hence the felt is valid even if note_tag is u32::MAX. + Felt::try_from(felt_int).expect("bytes should be a valid felt") } -fn unmerge_type_and_hint(value: u64) -> Result<(NoteType, NoteExecutionHint), NoteError> { - let high_nibble = ((value >> 38) & 0b11) as u8; - let tag_byte = (value & 0b111111) as u8; - let payload_u32 = (value >> 6 & 0xffffffff) as u32; +/// Unmerges the given felt into a [`NoteExecutionHint`] payload and a [`NoteTag`] and constructs a +/// [`NoteExecutionHint`] from the unmerged payload and the given `note_execution_hint_tag`. +fn unmerge_note_tag_and_hint_payload( + element: Felt, + note_execution_hint_tag: u8, +) -> Result<(NoteExecutionHint, NoteTag), NoteError> { + let element = element.as_int(); - let note_type = NoteType::try_from(high_nibble)?; - let note_execution_hint = NoteExecutionHint::from_parts(tag_byte, payload_u32)?; + let payload = (element >> 32) as u32; + let note_tag = (element & 0xffff_ffff) as u32; - Ok((note_type, note_execution_hint)) + let execution_hint = NoteExecutionHint::from_parts(note_execution_hint_tag, payload)?; + let note_tag = NoteTag::from(note_tag); + + Ok((execution_hint, note_tag)) } // TESTS @@ -178,10 +290,41 @@ fn unmerge_type_and_hint(value: u64) -> Result<(NoteType, NoteExecutionHint), No #[cfg(test)] mod tests { + use anyhow::Context; + use super::*; + use crate::{notes::NoteExecutionMode, testing::account_id::ACCOUNT_ID_MAX_ONES}; + + #[test] + fn note_metadata_serde() -> anyhow::Result<()> { + // Use the Account ID with the maximum one bits to test if the merge function always + // produces valid felts. + let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap(); + let note_type = NoteType::Public; + let tag = NoteTag::from_account_id(sender, NoteExecutionMode::Local).unwrap(); + let aux = Felt::try_from(0xffff_ffff_0000_0000u64).unwrap(); + + for execution_hint in [ + NoteExecutionHint::always(), + NoteExecutionHint::none(), + NoteExecutionHint::on_block_slot(10, 11, 12), + NoteExecutionHint::after_block(u32::MAX - 1).unwrap(), + ] { + let metadata = NoteMetadata::new(sender, note_type, tag, execution_hint, aux).unwrap(); + NoteMetadata::read_from_bytes(&metadata.to_bytes()) + .context(format!("failed for execution hint {execution_hint:?}"))?; + } + + Ok(()) + } #[test] - fn test_merge_and_unmerge() { + fn merge_and_unmerge_id_type_and_hint() { + // Use the Account ID with the maximum one bits to test if the merge function always + // produces valid felts. + let sender = AccountId::try_from(ACCOUNT_ID_MAX_ONES).unwrap(); + let sender_second_felt = sender.second_felt(); + let note_type = NoteType::Public; let note_execution_hint = NoteExecutionHint::OnBlockSlot { epoch_len: 10, @@ -189,30 +332,37 @@ mod tests { slot_offset: 12, }; - let merged_value = merge_type_and_hint(note_type, note_execution_hint); - let (extracted_note_type, extracted_note_execution_hint) = - unmerge_type_and_hint(merged_value).unwrap(); + let merged_value = + merge_id_type_and_hint_tag(sender_second_felt, note_type, note_execution_hint); + let (extracted_second_felt, extracted_note_type, extracted_note_execution_hint_tag) = + unmerge_id_type_and_hint_tag(merged_value).unwrap(); assert_eq!(note_type, extracted_note_type); - assert_eq!(note_execution_hint, extracted_note_execution_hint); + assert_eq!(note_execution_hint.into_parts().0, extracted_note_execution_hint_tag); + assert_eq!(sender_second_felt, extracted_second_felt); let note_type = NoteType::Private; let note_execution_hint = NoteExecutionHint::Always; - let merged_value = merge_type_and_hint(note_type, note_execution_hint); - let (extracted_note_type, extracted_note_execution_hint) = - unmerge_type_and_hint(merged_value).unwrap(); + let merged_value = + merge_id_type_and_hint_tag(sender_second_felt, note_type, note_execution_hint); + let (extracted_second_felt, extracted_note_type, extracted_note_execution_hint_tag) = + unmerge_id_type_and_hint_tag(merged_value).unwrap(); assert_eq!(note_type, extracted_note_type); - assert_eq!(note_execution_hint, extracted_note_execution_hint); + assert_eq!(note_execution_hint.into_parts().0, extracted_note_execution_hint_tag); + assert_eq!(sender_second_felt, extracted_second_felt); let note_type = NoteType::Private; let note_execution_hint = NoteExecutionHint::None; - let merged_value = merge_type_and_hint(note_type, note_execution_hint); - let (extracted_note_type, extracted_note_execution_hint) = - unmerge_type_and_hint(merged_value).unwrap(); + let merged_value = + merge_id_type_and_hint_tag(sender_second_felt, note_type, note_execution_hint); + let (extracted_second_felt, extracted_note_type, extracted_note_execution_hint_tag) = + unmerge_id_type_and_hint_tag(merged_value).unwrap(); + assert_eq!(note_type, extracted_note_type); - assert_eq!(note_execution_hint, extracted_note_execution_hint); + assert_eq!(note_execution_hint.into_parts().0, extracted_note_execution_hint_tag); + assert_eq!(sender_second_felt, extracted_second_felt); } } diff --git a/objects/src/notes/mod.rs b/objects/src/notes/mod.rs index 0e675d7f5..c3e7acebc 100644 --- a/objects/src/notes/mod.rs +++ b/objects/src/notes/mod.rs @@ -24,7 +24,7 @@ mod metadata; pub use metadata::NoteMetadata; mod execution_hint; -pub use execution_hint::NoteExecutionHint; +pub use execution_hint::{AfterBlockNumber, NoteExecutionHint}; mod note_id; pub use note_id::NoteId; diff --git a/objects/src/notes/note_tag.rs b/objects/src/notes/note_tag.rs index d60f746bd..f1bed70d6 100644 --- a/objects/src/notes/note_tag.rs +++ b/objects/src/notes/note_tag.rs @@ -12,10 +12,10 @@ use super::{ const NETWORK_EXECUTION: u8 = 0; const LOCAL_EXECUTION: u8 = 1; -// The 2 most significant bits are set to `0b11` -const LOCAL_EXECUTION_WITH_ALL_NOTE_TYPES_ALLOWED: u32 = 0xc0000000; -// The 2 most significant bits are set to `0b10` -const PUBLIC_USECASE: u32 = 0x80000000; +// The 2 most significant bits are set to `0b11`. +const LOCAL_EXECUTION_WITH_ALL_NOTE_TYPES_ALLOWED: u32 = 0xc000_0000; +// The 2 most significant bits are set to `0b10`. +const PUBLIC_USECASE: u32 = 0x8000_0000; /// [super::Note]'s execution mode hints. /// @@ -38,7 +38,7 @@ pub enum NoteExecutionMode { /// [NoteTag]`s are best effort filters for notes registered with the network. /// -/// Tags are light-weight values used to speed up queries. The 2 most signification bits of the tags +/// Tags are light-weight values used to speed up queries. The 2 most significant bits of the tags /// have the following interpretation: /// /// | Prefix | Execution hint | Target | Allowed [NoteType] | @@ -50,12 +50,13 @@ pub enum NoteExecutionMode { /// /// Where: /// -/// - [NoteExecutionMode] is set to [NoteExecutionMode::Network] to hint a [super::Note] should be -/// consumed by the network. These notes will be further validated and if possible consumed by it. +/// - [`NoteExecutionMode`] is set to [`NoteExecutionMode::Network`] to hint a [`Note`](super::Note) +/// should be consumed by the network. These notes will be further validated and if possible +/// consumed by it. /// - Target describes how to further interpret the bits in the tag. For tags with a specific -/// target, the rest of the tag is interpreted as an account_id. For use case values, the meaning -/// of the rest of the tag is not specified by the protocol and can be used by applications built -/// on top of the rollup. +/// target, the rest of the tag is interpreted as a partial [`AccountId`]. For use case values, +/// the meaning of the rest of the tag is not specified by the protocol and can be used by +/// applications built on top of the rollup. /// /// The note type is the only value enforced by the protocol. The rationale is that any note /// intended to be consumed by the network must be public to have all the details available. The @@ -80,12 +81,10 @@ impl NoteTag { /// The tag is constructed as follows: /// /// - For local execution, the two most significant bits are set to `0b11`, which allows for any - /// note type to be used, the following 14 bits are set to the 14 most significant bits of the + /// note type to be used. The following 14 bits are set to the most significant bits of the /// account ID, and the remaining 16 bits are set to 0. - /// - For network execution, the most significant bit is set to `0b0` and the remaining bits are - /// set to the 31 most significant bits of the account ID. Note that this results in the two - /// most significant bits of the tag being set to `0b00`, because the network execution - /// requires a public account which always have the high bit set to 0. + /// - For network execution, the most significant bits are set to `0b00` and the remaining bits + /// are set to the 30 most significant bits of the account ID. /// /// # Errors /// @@ -97,22 +96,39 @@ impl NoteTag { ) -> Result { match execution { NoteExecutionMode::Local => { - let id: u64 = account_id.into(); - // select 14 most significant bits of the account ID and shift them right by 2 bits - let high_bits = (id >> 34) as u32 & 0xffff0000; + let first_felt_id: u64 = account_id.first_felt().into(); + + // Shift the high bits of the account ID such that they are layed out as: + // [34 zero bits | remaining high bits (30 bits)]. + let high_bits = first_felt_id >> 34; + + // This is equivalent to the following layout, interpreted as a u32: + // [2 zero bits | remaining high bits (30 bits)]. + let high_bits = high_bits as u32; + + // Select the upper half of the u32 which then contains the 14 most significant bits + // of the account ID, i.e.: + // [2 zero bits | remaining high bits (14 bits) | 16 zero bits]. + let high_bits = high_bits & 0xffff0000; + + // Set the local execution tag in the two most significant bits. Ok(Self(high_bits | LOCAL_EXECUTION_WITH_ALL_NOTE_TYPES_ALLOWED)) }, NoteExecutionMode::Network => { if !account_id.is_public() { Err(NoteError::NetworkExecutionRequiresOnChainAccount) } else { - let id: u64 = account_id.into(); - // select 31 most significant bits of account ID and shift them right by 1 bit - let high_bits = (id >> 33) as u32; - // the tag will have the form 0 + 31 high bits of account ID; note that the - // second bit of the tag is guaranteed to be 0 because public account IDs start - // with 0 - Ok(Self(high_bits)) + let first_felt_id: u64 = account_id.first_felt().into(); + + // Shift the high bits of the account ID such that they are layed out as: + // [34 zero bits | remaining high bits (30 bits)]. + let high_bits = first_felt_id >> 34; + + // This is equivalent to the following layout, interpreted as a u32: + // [2 zero bits | remaining high bits (30 bits)]. + // The two most significant zero bits match the tag we need for network + // execution. + Ok(Self(high_bits as u32)) } }, } @@ -183,10 +199,9 @@ impl NoteTag { /// Returns note execution mode defined by this tag. /// - /// If the most significant bit of the tag is 0 or the 3 most significant bits are equal to - /// 0b101, the note is intended for local execution; otherwise, the note is intended for - /// network execution. - pub fn execution_hint(&self) -> NoteExecutionMode { + /// If the most significant bit of the tag is 0 the note is intended for local execution; + /// otherwise, the note is intended for network execution. + pub fn execution_mode(&self) -> NoteExecutionMode { let first_bit = self.0 >> 31; if first_bit == (LOCAL_EXECUTION as u32) { @@ -207,7 +222,7 @@ impl NoteTag { /// Returns an error if this tag is not consistent with the specified note type, and self /// otherwise. pub fn validate(&self, note_type: NoteType) -> Result { - if self.execution_hint() == NoteExecutionMode::Network && note_type != NoteType::Public { + if self.execution_mode() == NoteExecutionMode::Network && note_type != NoteType::Public { return Err(NoteError::NetworkExecutionRequiresPublicNote(note_type)); } @@ -297,21 +312,19 @@ mod tests { use super::{NoteExecutionMode, NoteTag}; use crate::{ - accounts::{ - account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, - ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, - ACCOUNT_ID_OFF_CHAIN_SENDER, ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, - ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN_2, - ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, - ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, - ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2, ACCOUNT_ID_SENDER, - }, - AccountId, - }, + accounts::AccountId, notes::NoteType, + testing::account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN, + ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1, + ACCOUNT_ID_OFF_CHAIN_SENDER, ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN_2, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2, ACCOUNT_ID_SENDER, + }, NoteError, }; @@ -349,7 +362,7 @@ mod tests { let tag = NoteTag::from_account_id(on_chain, NoteExecutionMode::Network) .expect("tag generation must work with network execution and on-chain account id"); assert!(tag.is_single_target()); - assert_eq!(tag.execution_hint(), NoteExecutionMode::Network); + assert_eq!(tag.execution_mode(), NoteExecutionMode::Network); tag.validate(NoteType::Public) .expect("network execution should require notes to be public"); @@ -367,7 +380,7 @@ mod tests { let tag = NoteTag::from_account_id(off_chain, NoteExecutionMode::Local) .expect("tag generation must work with local execution and off-chain account id"); assert!(!tag.is_single_target()); - assert_eq!(tag.execution_hint(), NoteExecutionMode::Local); + assert_eq!(tag.execution_mode(), NoteExecutionMode::Local); tag.validate(NoteType::Public) .expect("local execution should support public notes"); @@ -381,7 +394,7 @@ mod tests { let tag = NoteTag::from_account_id(on_chain, NoteExecutionMode::Local) .expect("Tag generation must work with local execution and on-chain account id"); assert!(!tag.is_single_target()); - assert_eq!(tag.execution_hint(), NoteExecutionMode::Local); + assert_eq!(tag.execution_mode(), NoteExecutionMode::Local); tag.validate(NoteType::Public) .expect("local execution should support public notes"); @@ -394,14 +407,33 @@ mod tests { #[test] fn test_from_account_id_values() { - let off_chain = - AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN).unwrap(); - let on_chain = - AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(); + // Off-Chain Account ID with the following bit pattern in the first and second byte: + // 0b11001100_01010101 + // ^^^^^^^^ ^^^^^^ <- these are the 14 bits used in the tag. + const OFF_CHAIN_INT: u128 = ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN + | 0x0055_0000_0000_0000_0000_0000_0000_0000; + let off_chain = AccountId::try_from(OFF_CHAIN_INT).unwrap(); + + // Expected off-chain tag with LOCAL_EXECUTION_WITH_ALL_NOTE_TYPES_ALLOWED. + let expected_off_chain_local_tag = NoteTag(0b11110011_00010101_00000000_00000000); + + // On-Chain Account ID with the following bit pattern in the first and second byte: + // 0b10101010_01010101_11001100_01110111 + // ^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^^^^ <- 30 bits of the network tag. + // ^^^^^^^^ ^^^^^^ <- 14 bits of the local tag. + let on_chain_int = ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN + | 0x0055_cc77_0000_0000_0000_0000_0000_0000; + let on_chain = AccountId::try_from(on_chain_int).unwrap(); + + // Expected on-chain tag with LOCAL_EXECUTION_WITH_ALL_NOTE_TYPES_ALLOWED. + let expected_on_chain_local_tag = NoteTag(0b11101010_10010101_00000000_00000000); + + // Expected on-chain tag with leading 00 tag bits for network execution. + let expected_on_chain_network_tag = NoteTag(0b00101010_10010101_01110011_00011101); assert_eq!( NoteTag::from_account_id(on_chain, NoteExecutionMode::Network).unwrap(), - NoteTag(0b00000000_00000000_00000000_00000000) + expected_on_chain_network_tag, ); assert_matches!( NoteTag::from_account_id(off_chain, NoteExecutionMode::Network), @@ -410,11 +442,13 @@ mod tests { assert_eq!( NoteTag::from_account_id(off_chain, NoteExecutionMode::Local).unwrap(), - NoteTag(0b11100100_00000000_00000000_00000000) + expected_off_chain_local_tag, ); + + // We expect the 16th most significant bit to be cut off. assert_eq!( NoteTag::from_account_id(on_chain, NoteExecutionMode::Local).unwrap(), - NoteTag(0b11000000_00000000_00000000_00000000) + expected_on_chain_local_tag, ); } @@ -509,44 +543,4 @@ mod tests { NoteError::NoteTagUseCaseTooLarge(use_case) if use_case == 1 << 14 ); } - - /// Test for assumption built in the [NoteTag] encoding that only on-chain accounts have the - /// highbit set to 0. If the account id encoding ever changes, the note tag needs to be - /// adjusted. - #[test] - fn test_only_onchain_account_have_the_highbit_set_to_zero() { - // Create a list of valid account ids with every combination of account types - let accounts = [ - // ON-CHAIN - // --------------------------------------------------------------------------- - AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(), - AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN_2).unwrap(), - AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN).unwrap(), - AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2).unwrap(), - AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(), - AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1).unwrap(), - AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2).unwrap(), - AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3).unwrap(), - AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(), - AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1).unwrap(), - // OFF-CHAIN - // -------------------------------------------------------------------------- - AccountId::try_from(ACCOUNT_ID_SENDER).unwrap(), - AccountId::try_from(ACCOUNT_ID_OFF_CHAIN_SENDER).unwrap(), - AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN).unwrap(), - AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap(), - AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap(), - ]; - - for acct in accounts { - let highbit = u64::from(acct) >> 63; - let onchain = highbit == 0; - - assert_eq!( - acct.is_public(), - onchain, - "the account_id encoding changed, this breaks the assumptions built in the NoteTag" - ); - } - } } diff --git a/objects/src/testing/account.rs b/objects/src/testing/account.rs index 4ef29033b..0d18da714 100644 --- a/objects/src/testing/account.rs +++ b/objects/src/testing/account.rs @@ -3,15 +3,16 @@ use vm_core::FieldElement; use super::constants::{self, FUNGIBLE_ASSET_AMOUNT, NON_FUNGIBLE_ASSET_DATA}; use crate::{ - accounts::{ - account_id::testing::{ + accounts::{Account, AccountCode, AccountId, AccountStorage, StorageMap, StorageSlot}, + assets::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}, + testing::{ + account_component::AccountMockComponent, + account_id::{ ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2, }, - Account, AccountCode, AccountId, AccountStorage, StorageMap, StorageSlot, + storage::FAUCET_STORAGE_DATA_SLOT, }, - assets::{Asset, AssetVault, FungibleAsset, NonFungibleAsset}, - testing::{account_component::AccountMockComponent, storage::FAUCET_STORAGE_DATA_SLOT}, Felt, Word, ZERO, }; @@ -20,7 +21,7 @@ use crate::{ impl Account { /// Creates a non-new mock account with a defined number of assets and storage - pub fn mock(account_id: u64, nonce: Felt, assembler: Assembler) -> Self { + pub fn mock(account_id: u128, nonce: Felt, assembler: Assembler) -> Self { let account_vault = if nonce == Felt::ZERO { AssetVault::default() } else { @@ -41,7 +42,7 @@ impl Account { } pub fn mock_fungible_faucet( - account_id: u64, + account_id: u128, nonce: Felt, initial_balance: Felt, assembler: Assembler, @@ -63,7 +64,7 @@ impl Account { } pub fn mock_non_fungible_faucet( - account_id: u64, + account_id: u128, nonce: Felt, empty_reserved_slot: bool, assembler: Assembler, diff --git a/objects/src/testing/account_id.rs b/objects/src/testing/account_id.rs new file mode 100644 index 000000000..8784cb5d1 --- /dev/null +++ b/objects/src/testing/account_id.rs @@ -0,0 +1,122 @@ +use crate::accounts::{AccountId, AccountStorageMode, AccountType}; + +// CONSTANTS +// -------------------------------------------------------------------------------------------- + +// REGULAR ACCOUNTS - OFF-CHAIN +pub const ACCOUNT_ID_SENDER: u128 = account_id::( + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + 0xfabb_ccde, +); +pub const ACCOUNT_ID_OFF_CHAIN_SENDER: u128 = account_id::( + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + 0xbfcc_dcee, +); +pub const ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN: u128 = account_id::( + AccountType::RegularAccountUpdatableCode, + AccountStorageMode::Private, + 0xccdd_eeff, +); +// REGULAR ACCOUNTS - ON-CHAIN +pub const ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN: u128 = account_id::( + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + 0xaabb_ccdd, +); +pub const ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN_2: u128 = account_id::( + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + 0xbbcc_ddee, +); +pub const ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN: u128 = account_id::( + AccountType::RegularAccountUpdatableCode, + AccountStorageMode::Public, + 0xacdd_eefc, +); +pub const ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN_2: u128 = account_id::( + AccountType::RegularAccountUpdatableCode, + AccountStorageMode::Public, + 0xeeff_ccdd, +); + +// These faucet IDs all have a unique first and second felt. This is to ensure that when they +// are used to issue an asset they don't cause us to run into the "multiple leaf" case when +// calling std::collections::smt::{set,get} which doesn't support the "multiple leaf" case at +// this time. + +// FUNGIBLE TOKENS - OFF-CHAIN +pub const ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN: u128 = + account_id::(AccountType::FungibleFaucet, AccountStorageMode::Private, 0xfabb_cddd); +// FUNGIBLE TOKENS - ON-CHAIN +pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN: u128 = + account_id::(AccountType::FungibleFaucet, AccountStorageMode::Public, 0xaabc_bcde); +pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_1: u128 = + account_id::(AccountType::FungibleFaucet, AccountStorageMode::Public, 0xbaca_ddef); +pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_2: u128 = + account_id::(AccountType::FungibleFaucet, AccountStorageMode::Public, 0xccdb_eefa); +pub const ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN_3: u128 = + account_id::(AccountType::FungibleFaucet, AccountStorageMode::Public, 0xeeff_cc99); + +// NON-FUNGIBLE TOKENS - OFF-CHAIN +pub const ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN: u128 = + account_id::(AccountType::NonFungibleFaucet, AccountStorageMode::Private, 0xaabc_ccde); +// NON-FUNGIBLE TOKENS - ON-CHAIN +pub const ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN: u128 = + account_id::(AccountType::NonFungibleFaucet, AccountStorageMode::Public, 0xbcca_ddef); +pub const ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1: u128 = + account_id::(AccountType::NonFungibleFaucet, AccountStorageMode::Public, 0xccdf_eefa); + +// TEST ACCOUNT IDs WITH CERTAIN PROPERTIES +/// The Account Id with the maximum possible one bits. +pub const ACCOUNT_ID_MAX_ONES: u128 = + account_id::(AccountType::NonFungibleFaucet, AccountStorageMode::Private, 0) + | 0x7fff_ffff_ffff_ff00_7fff_ffff_ffff_ff00; +/// The Account Id with the maximum possible zero bits. +pub const ACCOUNT_ID_MAX_ZEROES: u128 = + account_id::(AccountType::NonFungibleFaucet, AccountStorageMode::Private, 0x001f_0000); + +// UTILITIES +// -------------------------------------------------------------------------------------------- + +/// Produces a valid account ID with the given account type and storage mode. +/// +/// - Version is set to 0. +/// - Anchor epoch is set to 0. +/// +/// Finally, distributes the given `random` value over the ID to produce non-trivial values for +/// testing. This is easiest explained with an example. Suppose `random` is `0xaabb_ccdd`, +/// then the layout of the generated ID will be: +/// +/// ```text +/// 1st felt: [0xaa | 5 zero bytes | 0xbb | metadata byte] +/// 2nd felt: [2 zero bytes (epoch) | 0xcc | 3 zero bytes | 0xdd | zero byte] +/// ``` +pub const fn account_id( + account_type: AccountType, + storage_mode: AccountStorageMode, + random: u32, +) -> u128 { + let mut first_felt: u64 = 0; + + first_felt |= (account_type as u64) << AccountId::TYPE_SHIFT; + first_felt |= (storage_mode as u64) << AccountId::STORAGE_MODE_SHIFT; + + // Produce non-trivial IDs by distributing the random value. + let random_1st_felt_upper = random & 0xff00_0000; + let random_1st_felt_lower = random & 0x00ff_0000; + let random_2nd_felt_upper = random & 0x0000_ff00; + let random_2nd_felt_lower = random & 0x0000_00ff; + + // Shift the random part of the ID to start at the most significant end. + first_felt |= (random_1st_felt_upper as u64) << 32; + first_felt |= (random_1st_felt_lower as u64) >> 8; + + let mut id = (first_felt as u128) << 64; + + id |= (random_2nd_felt_upper as u128) << 32; + id |= (random_2nd_felt_lower as u128) << 8; + + id +} diff --git a/objects/src/testing/assets.rs b/objects/src/testing/assets.rs index 1a58f76e5..f69d2ebcc 100644 --- a/objects/src/testing/assets.rs +++ b/objects/src/testing/assets.rs @@ -1,13 +1,11 @@ use rand::{distributions::Standard, Rng}; use crate::{ - accounts::{ - account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, - }, - AccountId, AccountType, - }, + accounts::{AccountId, AccountIdPrefix, AccountType}, assets::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}, + testing::account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, + }, AssetError, }; @@ -15,7 +13,7 @@ use crate::{ /// times. #[derive(Debug, Clone)] pub struct NonFungibleAssetDetailsBuilder { - faucet_id: AccountId, + faucet_id: AccountIdPrefix, rng: T, } @@ -27,7 +25,7 @@ pub struct FungibleAssetBuilder { } impl NonFungibleAssetDetailsBuilder { - pub fn new(faucet_id: AccountId, rng: T) -> Result { + pub fn new(faucet_id: AccountIdPrefix, rng: T) -> Result { if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) { return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id)); } @@ -48,7 +46,7 @@ pub struct NonFungibleAssetBuilder { } impl NonFungibleAssetBuilder { - pub fn new(faucet_id: AccountId, rng: T) -> Result { + pub fn new(faucet_id: AccountIdPrefix, rng: T) -> Result { let details_builder = NonFungibleAssetDetailsBuilder::new(faucet_id, rng)?; Ok(Self { details_builder }) } @@ -93,7 +91,7 @@ impl NonFungibleAsset { /// Returns a mocked non-fungible asset, issued by [ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN]. pub fn mock(asset_data: &[u8]) -> Asset { let non_fungible_asset_details = NonFungibleAssetDetails::new( - AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(), + AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN).unwrap().prefix(), asset_data.to_vec(), ) .unwrap(); @@ -113,7 +111,8 @@ impl FungibleAsset { pub fn mock(amount: u64) -> Asset { Asset::Fungible( FungibleAsset::new( - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().expect("id is valid"), + AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN) + .expect("id should be valid"), amount, ) .expect("asset is valid"), diff --git a/objects/src/testing/block.rs b/objects/src/testing/block.rs index b54bcbe0e..6a324b4f3 100644 --- a/objects/src/testing/block.rs +++ b/objects/src/testing/block.rs @@ -29,7 +29,7 @@ impl BlockHeader { if acct.is_new() { None } else { - let felt_id: Felt = acct.id().into(); + let felt_id: Felt = acct.id().prefix().into(); Some((felt_id.as_int(), *acct.hash())) } }) diff --git a/objects/src/testing/mod.rs b/objects/src/testing/mod.rs index 4db0c4d45..27e450fbd 100644 --- a/objects/src/testing/mod.rs +++ b/objects/src/testing/mod.rs @@ -8,6 +8,7 @@ use vm_core::Word; pub mod account; pub mod account_code; pub mod account_component; +pub mod account_id; pub mod assets; pub mod block; pub mod constants; diff --git a/objects/src/testing/storage.rs b/objects/src/testing/storage.rs index 6b211e737..61ac2e26e 100644 --- a/objects/src/testing/storage.rs +++ b/objects/src/testing/storage.rs @@ -8,16 +8,16 @@ use vm_processor::Digest; use super::{constants::FUNGIBLE_FAUCET_INITIAL_BALANCE, prepare_word}; use crate::{ accounts::{ - account_id::testing::{ - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, - ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, - ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, - }, - get_account_seed_single, Account, AccountId, AccountStorage, AccountStorageDelta, + Account, AccountId, AccountIdAnchor, AccountIdVersion, AccountStorage, AccountStorageDelta, AccountStorageMode, AccountType, StorageMap, StorageMapDelta, StorageSlot, }, notes::NoteAssets, - AccountDeltaError, + testing::account_id::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_ON_CHAIN, + }, + AccountDeltaError, BlockHeader, }; // ACCOUNT STORAGE DELTA BUILDER @@ -138,10 +138,13 @@ pub enum AccountSeedType { } /// Returns the account id and seed for the specified account type. +/// +/// TODO: Not all variants are needed anymore, remove unneeded parts. pub fn generate_account_seed( account_seed_type: AccountSeedType, + anchor_block_header: &BlockHeader, assembler: Assembler, -) -> (AccountId, Word) { +) -> (Account, AccountId, Word) { let init_seed: [u8; 32] = Default::default(); let (account, account_type) = match account_seed_type { @@ -199,19 +202,27 @@ pub fn generate_account_seed( ), }; - let seed = get_account_seed_single( + let seed = AccountId::compute_account_seed( init_seed, account_type, AccountStorageMode::Public, + AccountIdVersion::VERSION_0, account.code().commitment(), account.storage().commitment(), + anchor_block_header.hash(), ) .unwrap(); + let anchor = AccountIdAnchor::try_from(anchor_block_header).unwrap(); let account_id = - AccountId::new(seed, account.code().commitment(), account.storage().commitment()).unwrap(); + AccountId::new(seed, anchor, account.code().commitment(), account.storage().commitment()) + .unwrap(); + + // Overwrite old ID with generated ID. + let (_, vault, storage, code, nonce) = account.into_parts(); + let account = Account::from_parts(account_id, vault, storage, code, nonce); - (account_id, seed) + (account, account_id, seed) } // UTILITIES diff --git a/objects/src/transaction/inputs.rs b/objects/src/transaction/inputs.rs index 50d33fe5b..9072a8fbf 100644 --- a/objects/src/transaction/inputs.rs +++ b/objects/src/transaction/inputs.rs @@ -3,7 +3,8 @@ use core::fmt::Debug; use super::{BlockHeader, ChainMmr, Digest, Felt, Hasher, Word}; use crate::{ - accounts::{Account, AccountId}, + accounts::{Account, AccountId, AccountIdAnchor}, + block::block_num_from_epoch, notes::{Note, NoteId, NoteInclusionProof, NoteLocation, Nullifier}, utils::serde::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, TransactionInputError, MAX_INPUT_NOTES_PER_TX, @@ -39,7 +40,7 @@ impl TransactionInputs { input_notes: InputNotes, ) -> Result { // validate the seed - validate_account_seed(&account, account_seed)?; + validate_account_seed(&account, &block_header, &block_chain, account_seed)?; // check the block_chain and block_header are consistent let block_num = block_header.block_num(); @@ -464,16 +465,43 @@ impl Deserializable for InputNote { } } +// INPUT NOTE +// ================================================================================================ + /// Validates that the provided seed is valid for this account. pub fn validate_account_seed( account: &Account, + block_header: &BlockHeader, + block_chain: &ChainMmr, account_seed: Option, ) -> Result<(), TransactionInputError> { match (account.is_new(), account_seed) { (true, Some(seed)) => { - let account_id = - AccountId::new(seed, account.code().commitment(), account.storage().commitment()) - .map_err(TransactionInputError::InvalidAccountIdSeed)?; + let anchor_block_number = block_num_from_epoch(account.id().anchor_epoch()); + + let anchor_block_hash = if block_header.block_num() == anchor_block_number { + block_header.hash() + } else { + let anchor_block_header = + block_chain.get_block(anchor_block_number).ok_or_else(|| { + TransactionInputError::AnchorBlockHeaderNotProvidedForNewAccount( + account.id().anchor_epoch(), + ) + })?; + anchor_block_header.hash() + }; + + let anchor = AccountIdAnchor::new(anchor_block_number, anchor_block_hash) + .map_err(TransactionInputError::InvalidAccountIdSeed)?; + + let account_id = AccountId::new( + seed, + anchor, + account.code().commitment(), + account.storage().commitment(), + ) + .map_err(TransactionInputError::InvalidAccountIdSeed)?; + if account_id != account.id() { return Err(TransactionInputError::InconsistentAccountSeed { expected: account.id(), diff --git a/objects/src/transaction/proven_tx.rs b/objects/src/transaction/proven_tx.rs index 2e6f66669..149464f7c 100644 --- a/objects/src/transaction/proven_tx.rs +++ b/objects/src/transaction/proven_tx.rs @@ -527,13 +527,13 @@ mod tests { use super::ProvenTransaction; use crate::{ accounts::{ - account_id::testing::ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, delta::AccountUpdateDetails, AccountDelta, AccountId, AccountStorageDelta, AccountVaultDelta, StorageMapDelta, }, + testing::account_id::ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, transaction::TxAccountUpdate, utils::Serializable, - Digest, Felt, ProvenTransactionError, ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, ONE, ZERO, + Digest, ProvenTransactionError, ACCOUNT_UPDATE_MAX_SIZE, EMPTY_WORD, ONE, ZERO, }; fn check_if_sync() {} @@ -565,7 +565,7 @@ mod tests { AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(ONE)).unwrap(); let details = AccountUpdateDetails::Delta(delta); TxAccountUpdate::new( - AccountId::new_unchecked(Felt::new(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN)), + AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(), Digest::new(EMPTY_WORD), Digest::new(EMPTY_WORD), details, @@ -594,7 +594,7 @@ mod tests { let details_size = details.get_size_hint(); let err = TxAccountUpdate::new( - AccountId::new_unchecked(Felt::new(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN)), + AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(), Digest::new(EMPTY_WORD), Digest::new(EMPTY_WORD), details,