diff --git a/Cargo.toml b/Cargo.toml index 81090b4..75869ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,10 @@ members = [ "crates/executor", "crates/peer", "crates/prover", - "crates/runner", "crates/tests", + "crates/runner", + "crates/compiler", + "crates/tests", + ] exclude = [] @@ -63,4 +66,5 @@ sharp-p2p-executor = { path = "crates/executor" } sharp-p2p-peer = { path = "crates/peer" } sharp-p2p-prover = { path = "crates/prover" } sharp-p2p-runner = { path = "crates/runner" } +sharp-p2p-compiler = { path = "crates/compiler" } sharp-p2p-tests = { path = "crates/tests" } diff --git a/cairo/bootloader/objects.py b/cairo/bootloader/objects.py index ada6173..ecabe98 100644 --- a/cairo/bootloader/objects.py +++ b/cairo/bootloader/objects.py @@ -13,19 +13,6 @@ from starkware.starkware_utils.marshmallow_dataclass_fields import additional_metadata from starkware.starkware_utils.validated_dataclass import ValidatedMarshmallowDataclass - -class TaskSpec(ValidatedMarshmallowDataclass): - """ - Contains task's specification. - """ - - @abstractmethod - def load_task(self) -> "Task": - """ - Returns the corresponding task. - """ - - class Task: @abstractmethod def get_program(self) -> ProgramBase: @@ -33,63 +20,23 @@ def get_program(self) -> ProgramBase: Returns the task's Cairo program. """ - -@marshmallow_dataclass.dataclass(frozen=True) -class RunProgramTask(TaskSpec, Task): - TYPE: ClassVar[str] = "RunProgramTask" - program: Program - program_input: dict - use_poseidon: bool - - def get_program(self) -> Program: - return self.program - - def load_task(self) -> "Task": - return self - - -@marshmallow_dataclass.dataclass(frozen=True) -class CairoPiePath(TaskSpec): - TYPE: ClassVar[str] = "CairoPiePath" - path: str - use_poseidon: bool - - def load_task(self) -> "CairoPieTask": - """ - Loads the PIE to memory. - """ - return CairoPieTask(cairo_pie=CairoPie.from_file(self.path), use_poseidon=self.use_poseidon) - - -class TaskSchema(OneOfSchema): - """ - Schema for Task/CairoPiePath. - OneOfSchema adds a "type" field. - """ - - type_schemas: Dict[str, Type[marshmallow.Schema]] = { - RunProgramTask.TYPE: RunProgramTask.Schema, - CairoPiePath.TYPE: CairoPiePath.Schema, - } - - def get_obj_type(self, obj): - return obj.TYPE - - @dataclasses.dataclass(frozen=True) -class CairoPieTask(Task): +class Job(Task): + reward: int + num_of_steps: int cairo_pie: CairoPie - use_poseidon: bool + registry_address: bytearray + public_key: bytearray + signature: bytearray def get_program(self) -> StrippedProgram: return self.cairo_pie.program - @marshmallow_dataclass.dataclass(frozen=True) class SimpleBootloaderInput(ValidatedMarshmallowDataclass): - tasks: List[TaskSpec] = field( - metadata=additional_metadata(marshmallow_field=mfields.List(mfields.Nested(TaskSchema))) - ) + identity: bytearray + job: Job + fact_topologies_path: Optional[str] # If true, the bootloader will put all the outputs in a single page, ignoring the diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 855c01b..adeb8ac 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -10,13 +10,14 @@ license-file.workspace = true [dependencies] bincode.workspace = true cairo-felt.workspace = true -futures.workspace= true +futures.workspace = true hex.workspace = true libp2p.workspace = true libsecp256k1.workspace = true num-bigint.workspace = true serde_json.workspace = true serde.workspace = true +strum.workspace = true tempfile.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/crates/runner/src/cairo_runner/types/layout.rs b/crates/common/src/layout.rs similarity index 100% rename from crates/runner/src/cairo_runner/types/layout.rs rename to crates/common/src/layout.rs diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 387df1a..e6170be 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -2,6 +2,7 @@ pub mod hash_macro; pub mod job; pub mod job_trace; pub mod job_witness; +pub mod layout; pub mod network; pub mod process; pub mod topic; diff --git a/crates/compiler/Cargo.toml b/crates/compiler/Cargo.toml new file mode 100644 index 0000000..346dddd --- /dev/null +++ b/crates/compiler/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "sharp-p2p-compiler" +version.workspace = true +edition.workspace = true +repository.workspace = true +license-file.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-process.workspace = true +cairo-proof-parser.workspace = true +futures.workspace = true +hex.workspace = true +itertools.workspace = true +libsecp256k1.workspace = true +rand.workspace = true +serde_json.workspace = true +serde.workspace = true +sharp-p2p-common.workspace = true +strum.workspace = true +tempfile.workspace = true +thiserror.workspace = true +tokio.workspace = true +tracing.workspace = true +zip-extensions.workspace = true diff --git a/crates/compiler/build.rs b/crates/compiler/build.rs new file mode 100644 index 0000000..6eb8507 --- /dev/null +++ b/crates/compiler/build.rs @@ -0,0 +1,16 @@ +use std::process::Command; + +fn main() { + // Check if cairo-run command is present + check_command("cairo-run"); + + // Check if cairo-compile command is present + check_command("cairo-compile"); +} + +fn check_command(cmd: &str) { + match Command::new(cmd).arg("--version").output() { + Ok(_) => println!("{} command found", cmd), + Err(e) => panic!("Failed to execute {} command: {}", cmd, e), + } +} diff --git a/crates/compiler/src/cairo_compiler/mod.rs b/crates/compiler/src/cairo_compiler/mod.rs new file mode 100644 index 0000000..862254f --- /dev/null +++ b/crates/compiler/src/cairo_compiler/mod.rs @@ -0,0 +1,113 @@ +use crate::{errors::CompilerControllerError, traits::CompilerController}; +use async_process::Stdio; +use futures::Future; +use sharp_p2p_common::layout::Layout; +use sharp_p2p_common::{job::Job, process::Process}; +use std::path::PathBuf; +use std::{io::Read, pin::Pin}; +use tempfile::NamedTempFile; +use tokio::{process::Command, select, sync::mpsc}; +use tracing::debug; + +pub mod tests; + +pub struct CairoCompiler {} + +impl CairoCompiler { + pub fn new() -> Self { + Self {} + } +} + +impl Default for CairoCompiler { + fn default() -> Self { + Self::new() + } +} + +impl CompilerController for CairoCompiler { + fn run( + &self, + program_path: PathBuf, + program_input_path: PathBuf, + ) -> Result>, CompilerControllerError> { + let (terminate_tx, mut terminate_rx) = mpsc::channel::<()>(10); + let future: Pin> + '_>> = + Box::pin(async move { + let layout: &str = Layout::RecursiveWithPoseidon.into(); + + let output = NamedTempFile::new()?; + + let mut task = Command::new("cairo-compile") + .arg(program_path.as_path()) + .arg("--output") + .arg(output.path()) + .arg("--proof_mode") + .stdout(Stdio::null()) + .spawn()?; + + debug!("program {:?} is compiling... ", program_path); + + loop { + select! { + output = task.wait() => { + debug!("{:?}", output); + if !output?.success() { + return Err(CompilerControllerError::TaskTerminated); + } + let output = task.wait_with_output().await?; + debug!("{:?}", output); + break; + } + Some(()) = terminate_rx.recv() => { + task.start_kill()?; + } + } + } + + // output + let mut cairo_pie = NamedTempFile::new()?; + + let mut task = Command::new("cairo-run") + .arg("--program") + .arg(output.path()) + .arg("--layout") + .arg(layout) + .arg("--program_input") + .arg(program_input_path.as_path()) + .arg("--cairo_pie_output") + .arg(cairo_pie.path()) + .arg("--print_output") + .stdout(Stdio::null()) + .spawn()?; + + debug!("program {:?} is generating PIE... ", program_path); + + loop { + select! { + output = task.wait() => { + debug!("{:?}", output); + if !output?.success() { + return Err(CompilerControllerError::TaskTerminated); + } + let output = task.wait_with_output().await?; + debug!("{:?}", output); + break; + } + Some(()) = terminate_rx.recv() => { + task.start_kill()?; + } + } + } + + // cairo run had finished + let mut cairo_pie_bytes = Vec::new(); + cairo_pie.read_to_end(&mut cairo_pie_bytes)?; + + // TODO: calculate details + Ok(Job { cairo_pie_compressed: cairo_pie_bytes, ..Default::default() }) + }); + + Ok(Process::new(future, terminate_tx)) + } +} diff --git a/crates/compiler/src/cairo_compiler/tests/mod.rs b/crates/compiler/src/cairo_compiler/tests/mod.rs new file mode 100644 index 0000000..e829339 --- /dev/null +++ b/crates/compiler/src/cairo_compiler/tests/mod.rs @@ -0,0 +1,6 @@ +pub mod models; + +#[cfg(all(test, feature = "full_test"))] +pub mod multiple_job; +#[cfg(test)] +pub mod single_job; diff --git a/crates/compiler/src/cairo_compiler/tests/models.rs b/crates/compiler/src/cairo_compiler/tests/models.rs new file mode 100644 index 0000000..d252648 --- /dev/null +++ b/crates/compiler/src/cairo_compiler/tests/models.rs @@ -0,0 +1,16 @@ +use std::{env, path::PathBuf}; + +pub struct TestFixture { + pub program_input_path: PathBuf, + pub program_path: PathBuf, +} + +pub fn fixture() -> TestFixture { + let ws_root = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR env not present")) + .join("../../"); + let program_path = ws_root.join("crates/tests/cairo/fibonacci.cairo"); + let program_input_path = ws_root.join("crates/tests/cairo/fibonacci_input.json"); + + TestFixture { program_path, program_input_path } +} diff --git a/crates/compiler/src/cairo_compiler/tests/multiple_job.rs b/crates/compiler/src/cairo_compiler/tests/multiple_job.rs new file mode 100644 index 0000000..9134181 --- /dev/null +++ b/crates/compiler/src/cairo_compiler/tests/multiple_job.rs @@ -0,0 +1,46 @@ +use crate::{ + cairo_compiler::{tests::models::fixture, CairoCompiler}, + traits::CompilerController, +}; +use futures::{stream::FuturesUnordered, StreamExt}; + +#[tokio::test] +async fn run_multiple_jobs() { + let fixture1 = fixture(); + let fixture2 = fixture(); + + let compiler = CairoCompiler::new(); + let mut futures = FuturesUnordered::new(); + + let job1 = compiler.run(fixture1.program_path, fixture1.program_input_path).unwrap(); + let job2 = compiler.run(fixture2.program_path, fixture2.program_input_path).unwrap(); + + futures.push(job1); + futures.push(job2); + + while let Some(job) = futures.next().await { + job.unwrap(); + } +} + +#[tokio::test] +async fn abort_multiple_jobs() { + let fixture1 = fixture(); + let fixture2 = fixture(); + + let runner = CairoCompiler::new(); + let mut futures = FuturesUnordered::new(); + + let job1 = runner.run(fixture1.program_path, fixture1.program_input_path).unwrap(); + let job2 = runner.run(fixture2.program_path, fixture2.program_input_path).unwrap(); + + job1.abort().await.unwrap(); + job2.abort().await.unwrap(); + + futures.push(job1); + futures.push(job2); + + while let Some(job_trace) = futures.next().await { + job_trace.unwrap_err(); + } +} diff --git a/crates/compiler/src/cairo_compiler/tests/single_job.rs b/crates/compiler/src/cairo_compiler/tests/single_job.rs new file mode 100644 index 0000000..06a4c55 --- /dev/null +++ b/crates/compiler/src/cairo_compiler/tests/single_job.rs @@ -0,0 +1,22 @@ +use crate::{ + cairo_compiler::{tests::models::fixture, CairoCompiler}, + traits::CompilerController, +}; + +#[tokio::test] +async fn run_single_job() { + let fixture = fixture(); + + let compiler = CairoCompiler::new(); + compiler.run(fixture.program_path, fixture.program_input_path).unwrap().await.unwrap(); +} + +#[tokio::test] +async fn abort_single_jobs() { + let fixture = fixture(); + + let runner = CairoCompiler::new(); + let job = runner.run(fixture.program_path, fixture.program_input_path).unwrap(); + job.abort().await.unwrap(); + job.await.unwrap_err(); +} diff --git a/crates/compiler/src/errors.rs b/crates/compiler/src/errors.rs new file mode 100644 index 0000000..6471017 --- /dev/null +++ b/crates/compiler/src/errors.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum CompilerControllerError { + #[error("task not found")] + TaskNotFound, + + #[error("task not found")] + TaskTerminated, + + #[error("io")] + Io(#[from] std::io::Error), + + #[error("serde")] + Serde(#[from] serde_json::Error), + + #[error("proof parsing error")] + ProofParseError(String), +} diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs new file mode 100644 index 0000000..7eb0d46 --- /dev/null +++ b/crates/compiler/src/lib.rs @@ -0,0 +1,4 @@ +pub mod cairo_compiler; +pub mod errors; +#[allow(async_fn_in_trait)] +pub mod traits; diff --git a/crates/compiler/src/traits.rs b/crates/compiler/src/traits.rs new file mode 100644 index 0000000..30aeb81 --- /dev/null +++ b/crates/compiler/src/traits.rs @@ -0,0 +1,19 @@ +use crate::errors::CompilerControllerError; +use sharp_p2p_common::{job::Job, process::Process}; +use std::path::PathBuf; + +/* + The `CompilerController` trait is responsible for taking a user's program and preparing a `Job` object. + This process involves compiling the user's code and creating a Cairo PIE (Proof-of-Inclusion-Execution) object from it. + The resulting `Job` object encapsulates the necessary information for later execution by a `RunnerController`. + The `run` method accepts the paths to the program and its input, returning a `Result` containing a `Process` object. + Upon successful completion, it yields a `Job` object, ready to be utilized by a `RunnerController` to execute the program. +*/ + +pub trait CompilerController { + fn run( + &self, + program_path: PathBuf, + program_input_path: PathBuf, + ) -> Result>, CompilerControllerError>; +} diff --git a/crates/prover/src/stone_prover/mod.rs b/crates/prover/src/stone_prover/mod.rs index e3b92ee..f1f278a 100644 --- a/crates/prover/src/stone_prover/mod.rs +++ b/crates/prover/src/stone_prover/mod.rs @@ -30,13 +30,12 @@ impl StoneProver { } impl ProverController for StoneProver { - type ProcessResult = Result; fn run( &self, job_trace: JobTrace, - ) -> Result, ProverControllerError> { + ) -> Result>, ProverControllerError> { let (terminate_tx, mut terminate_rx) = mpsc::channel::<()>(10); - let future: Pin + '_>> = + let future: Pin> + '_>> = Box::pin(async move { let mut out_file = NamedTempFile::new()?; diff --git a/crates/prover/src/traits.rs b/crates/prover/src/traits.rs index e70055a..6aa5b54 100644 --- a/crates/prover/src/traits.rs +++ b/crates/prover/src/traits.rs @@ -1,10 +1,18 @@ use crate::errors::ProverControllerError; -use sharp_p2p_common::{job_trace::JobTrace, process::Process}; +use sharp_p2p_common::{job_trace::JobTrace, job_witness::JobWitness, process::Process}; + +/* + The `ProverController` trait defines the behavior for creating zkSTARK proofs from a `JobTrace` obtained from a `RunnerController`. + It abstracts over the prover process for ease of management and allows for convenient abortion of execution. + The `run` method takes a `JobTrace` as input and returns a `Result` containing a `Process` object, + which serves as a handle for controlling the ongoing proving process. + Upon successful completion, it yields a `JobWitness` object, representing the serialized proof of correctness of the `JobTrace`. + This proof can be subsequently sent to a verifier for zkSTARK proof verification. +*/ pub trait ProverController { - type ProcessResult; fn run( &self, job_trace: JobTrace, - ) -> Result, ProverControllerError>; + ) -> Result>, ProverControllerError>; } diff --git a/crates/runner/src/cairo_runner/mod.rs b/crates/runner/src/cairo_runner/mod.rs index b90156c..934acf8 100644 --- a/crates/runner/src/cairo_runner/mod.rs +++ b/crates/runner/src/cairo_runner/mod.rs @@ -1,11 +1,8 @@ -use self::types::{ - input::{BootloaderInput, BootloaderTask}, - layout::Layout, -}; +use self::types::input::{BootloaderInput, BootloaderTask}; use crate::{errors::RunnerControllerError, traits::RunnerController}; use async_process::Stdio; use futures::Future; -use sharp_p2p_common::{hash, job::Job, job_trace::JobTrace, process::Process}; +use sharp_p2p_common::{hash, job::Job, job_trace::JobTrace, layout::Layout, process::Process}; use std::{ hash::{DefaultHasher, Hash, Hasher}, pin::Pin, @@ -29,10 +26,12 @@ impl CairoRunner { } impl RunnerController for CairoRunner { - type ProcessResult = Result; - fn run(&self, job: Job) -> Result, RunnerControllerError> { + fn run( + &self, + job: Job, + ) -> Result>, RunnerControllerError> { let (terminate_tx, mut terminate_rx) = mpsc::channel::<()>(10); - let future: Pin + '_>> = + let future: Pin> + '_>> = Box::pin(async move { let layout: &str = Layout::RecursiveWithPoseidon.into(); diff --git a/crates/runner/src/cairo_runner/types/mod.rs b/crates/runner/src/cairo_runner/types/mod.rs index 1d2c2cd..7839bc5 100644 --- a/crates/runner/src/cairo_runner/types/mod.rs +++ b/crates/runner/src/cairo_runner/types/mod.rs @@ -1,2 +1 @@ pub mod input; -pub mod layout; diff --git a/crates/runner/src/traits.rs b/crates/runner/src/traits.rs index eeccc5b..0b93cf0 100644 --- a/crates/runner/src/traits.rs +++ b/crates/runner/src/traits.rs @@ -1,7 +1,19 @@ use crate::errors::RunnerControllerError; -use sharp_p2p_common::{job::Job, process::Process}; +use sharp_p2p_common::{job::Job, job_trace::JobTrace, process::Process}; + +/* + The `RunnerController` trait defines the responsibility for executing a `Job` within a Cairo bootloader environment. + It ensures the validity of the `Job` object and embeds a witness of this validation in the program output. + This process guarantees that the `Job` was not maliciously created by any party. + The `run` method takes a `Job` as input and returns a `Result` containing a `Process` object. + Upon successful execution, it produces a `JobTrace` object, + encapsulating the execution trace of the `Job`, which can later be handled by the zkSTARK Prover. + The bootloader runs the `Job` as a task in proof mode with a selected layout to facilitate the creation of the `JobTrace`. +*/ pub trait RunnerController { - type ProcessResult; - fn run(&self, job: Job) -> Result, RunnerControllerError>; + fn run( + &self, + job: Job, + ) -> Result>, RunnerControllerError>; } diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index 9011b59..06bb165 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -11,7 +11,8 @@ license-file.workspace = true futures.workspace = true sharp-p2p-prover.workspace = true sharp-p2p-runner.workspace = true +sharp-p2p-compiler.workspace = true tokio.workspace = true [features] -full_test = [] \ No newline at end of file +full_test = [] diff --git a/crates/tests/cairo/fibonacci.cairo b/crates/tests/cairo/fibonacci.cairo new file mode 100644 index 0000000..6c05582 --- /dev/null +++ b/crates/tests/cairo/fibonacci.cairo @@ -0,0 +1,39 @@ +// Copyright 2023 StarkWare Industries Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.starkware.co/open-source-license/ +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions +// and limitations under the License. + +%builtins output +func main(output_ptr: felt*) -> (output_ptr: felt*) { + alloc_locals; + + // Load fibonacci_claim_index and copy it to the output segment. + local fibonacci_claim_index; + %{ ids.fibonacci_claim_index = program_input['fibonacci_claim_index'] %} + + assert output_ptr[0] = fibonacci_claim_index; + let res = fib(1, 1, fibonacci_claim_index); + assert output_ptr[1] = res; + + // Return the updated output_ptr. + return (output_ptr=&output_ptr[2]); +} + +func fib(first_element: felt, second_element: felt, n: felt) -> felt { + if (n == 0) { + return second_element; + } + + return fib( + first_element=second_element, second_element=first_element + second_element, n=n - 1 + ); +} diff --git a/crates/tests/cairo/fibonacci_input.json b/crates/tests/cairo/fibonacci_input.json new file mode 100644 index 0000000..d2fd606 --- /dev/null +++ b/crates/tests/cairo/fibonacci_input.json @@ -0,0 +1,3 @@ +{ + "fibonacci_claim_index": 10 +} diff --git a/crates/tests/src/tests/compiler_runner_flow.rs b/crates/tests/src/tests/compiler_runner_flow.rs new file mode 100644 index 0000000..2186768 --- /dev/null +++ b/crates/tests/src/tests/compiler_runner_flow.rs @@ -0,0 +1,60 @@ +use futures::stream::FuturesUnordered; +use futures::{FutureExt, StreamExt}; +use sharp_p2p_compiler::cairo_compiler::tests::models::fixture as compiler_fixture; +use sharp_p2p_compiler::cairo_compiler::CairoCompiler; +use sharp_p2p_compiler::traits::CompilerController; +use sharp_p2p_runner::cairo_runner::tests::models::fixture as runner_fixture; +use sharp_p2p_runner::cairo_runner::CairoRunner; +use sharp_p2p_runner::traits::RunnerController; + +#[tokio::test] +async fn run_single_job() { + let compiler_fixture = compiler_fixture(); + let runner_fixture = runner_fixture(); + + let compiler = CairoCompiler::new(); + let runner = CairoRunner::new(runner_fixture.program_path); + + compiler + .run(compiler_fixture.program_path, compiler_fixture.program_input_path) + .unwrap() + .map(|job| { + println!("job: {:?}", job); + runner.run(job.unwrap()).unwrap() + }) + .flatten() + .await + .unwrap(); +} + +#[tokio::test] +async fn run_multiple_job() { + let compiler_fixture1 = compiler_fixture(); + let compiler_fixture2 = compiler_fixture(); + let runner_fixture1 = runner_fixture(); + + let compiler = CairoCompiler::new(); + let runner = CairoRunner::new(runner_fixture1.program_path); + let mut futures = FuturesUnordered::new(); + + let job_trace1 = compiler + .run(compiler_fixture1.program_path, compiler_fixture1.program_input_path) + .unwrap() + .map(|job| runner.run(job.unwrap()).unwrap()) + .flatten() + .boxed_local(); + + let job_trace2 = compiler + .run(compiler_fixture2.program_path, compiler_fixture2.program_input_path) + .unwrap() + .map(|job| runner.run(job.unwrap()).unwrap()) + .flatten() + .boxed_local(); + + futures.push(job_trace1); + futures.push(job_trace2); + + while let Some(job_trace) = futures.next().await { + job_trace.unwrap(); + } +} diff --git a/crates/tests/src/tests/mod.rs b/crates/tests/src/tests/mod.rs index e84c1c8..e446130 100644 --- a/crates/tests/src/tests/mod.rs +++ b/crates/tests/src/tests/mod.rs @@ -1,2 +1,4 @@ #[cfg(all(test, feature = "full_test"))] +mod compiler_runner_flow; +#[cfg(all(test, feature = "full_test"))] mod runner_prover_flow;