diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 1dc6019..712018a 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -49,4 +49,7 @@ jobs: sphinx-build -b doctest docs/source/ _readthedocs/doctest - name: Test run: | - python test.py + python -m venv .env + source .env/bin/activate + maturin develop --release --features openssl/vendored + python python/test.py diff --git a/sdk/.gitignore b/sdk/.gitignore index 68947d4..c9f96e6 100644 --- a/sdk/.gitignore +++ b/sdk/.gitignore @@ -1,3 +1,5 @@ .env/ target/ +__pycache__/ +python/aleo/_aleolib.abi3.so _readthedocs/ diff --git a/sdk/Cargo.lock b/sdk/Cargo.lock index f59ff0d..e124744 100644 --- a/sdk/Cargo.lock +++ b/sdk/Cargo.lock @@ -43,6 +43,8 @@ name = "aleo" version = "0.2.0" dependencies = [ "anyhow", + "indexmap", + "once_cell", "openssl", "pyo3", "rand", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index b9b447f..e424d74 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -7,10 +7,13 @@ edition = "2021" license = "GPL-3.0-or-later" [lib] +name = "_aleolib" crate-type = ["cdylib"] [dependencies] anyhow = "1" +indexmap = "2.1.0" +once_cell = "1.18.0" openssl = "0.10" pyo3 = { version = "0.20.0", features = ["extension-module", "abi3-py37", "anyhow"] } rand = { version = "^0.8" } diff --git a/sdk/install.sh b/sdk/install.sh index 9f37efe..8532aff 100755 --- a/sdk/install.sh +++ b/sdk/install.sh @@ -7,4 +7,4 @@ python3 -m venv .env source .env/bin/activate pip install maturin maturin develop -python test.py +python python/test.py diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 0533d47..daeddda 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -1,14 +1,28 @@ +[project] +name = "aleo" +description = "A Python SDK for zero-knowledge cryptography based on Aleo" +version = "0.2.0" +readme = "Readme.md" +license = {file = "LICENSE.md"} +authors = [ + {name = "Konstantin Pandl"}, + {name = "Mike Turner"}, + {name = "Roman Proskuryakov"}, +] +classifiers = [ # Optional + "Programming Language :: Python :: 3", +] + +[project.urls] +"Homepage" = "https://github.com/AleoHQ/python-sdk" + [build-system] requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [tool.maturin] -name = "aleo" -version = "0.2.0" -description = "A Python SDK for zero-knowledge cryptography based on Aleo" -repository = "https://github.com/AleoHQ/python-sdk/tree/master/sdk" -license = "GPL-3.0-or-later" -authors = ["Konstantin Pandl", "Mike Turner", "Roman Proskuryakov"] +python-source = "python" +module-name = "aleo._aleolib" [tool.pyright] reportMissingModuleSource = false # don't report missing aleo.so for CI purposes diff --git a/sdk/python/aleo/__init__.py b/sdk/python/aleo/__init__.py new file mode 100644 index 0000000..bdcd128 --- /dev/null +++ b/sdk/python/aleo/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from ._aleolib import * +from .encryptor import * + +__doc__ = _aleolib.__doc__ diff --git a/sdk/aleo.pyi b/sdk/python/aleo/_aleolib.pyi similarity index 88% rename from sdk/aleo.pyi rename to sdk/python/aleo/_aleolib.pyi index 1df82fa..46dd2de 100644 --- a/sdk/aleo.pyi +++ b/sdk/python/aleo/_aleolib.pyi @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import List, Optional, Tuple +from typing import List, Mapping, Optional, Tuple class Account: @@ -32,6 +32,13 @@ class Boolean: def __new__(cls, b: bool) -> Boolean: ... +class Ciphertext: + @staticmethod + def from_string(s: str) -> Ciphertext: ... + def decrypt(self, view_key: ViewKey, nonce: Group) -> Plaintext: ... + def decrypt_symmetric(self, plaintext_view_key: Field) -> Plaintext: ... + + class Credits: def __new__(cls, value: float) -> Credits: ... def micro(self) -> MicroCredits: ... @@ -87,12 +94,18 @@ class Fee: class Field: + @staticmethod + def random() -> Field: ... @staticmethod def from_string(s: str) -> Field: ... @staticmethod + def domain_separator(domain: str) -> Field: ... + @staticmethod def from_u128(u128: int) -> Field: ... @staticmethod def zero() -> Field: ... + def __mul__(self, other: Field) -> Field: ... + def __truediv__(self, other: Field) -> Field: ... class Group: @@ -190,16 +203,48 @@ class Locator: def resource(self) -> Identifier: ... +class Network: + @staticmethod + def name() -> str: ... + @staticmethod + def version() -> int: ... + @staticmethod + def edition() -> int: ... + @staticmethod + def hash_psd2(input: List[Field]) -> Field: ... + + class MicroCredits: def __new__(cls, value: int) -> MicroCredits: ... def __int__(self) -> int: ... +class Plaintext: + @staticmethod + def from_string(s: str) -> Plaintext: ... + @staticmethod + def new_literal(literal: Literal) -> Plaintext: ... + @staticmethod + def new_struct(kv: List[Tuple[Identifier, Plaintext]]) -> Plaintext: ... + @staticmethod + def new_array(kv: List[Plaintext]) -> Plaintext: ... + def encrypt(self, address: Address, randomizer: Scalar) -> Ciphertext: ... + def encrypt_symmetric(self, plaintext_view_key: Field) -> Ciphertext: ... + def is_literal(self) -> bool: ... + def is_struct(self) -> bool: ... + def is_array(self) -> bool: ... + def as_literal(self) -> Literal: ... + def as_struct(self) -> Mapping[Identifier, Plaintext]: ... + def as_array(self) -> List[Plaintext]: ... + + class PrivateKey: def address(self) -> Address: ... def compute_key(self) -> ComputeKey: ... @staticmethod def from_string(private_key: str) -> PrivateKey: ... + @staticmethod + def from_seed(seed: Field) -> PrivateKey: ... def seed(self) -> Field: ... def sign(self, message: bytes) -> Signature: ... def sk_sig(self) -> Scalar: ... diff --git a/sdk/python/aleo/encryptor.py b/sdk/python/aleo/encryptor.py new file mode 100644 index 0000000..93b0dde --- /dev/null +++ b/sdk/python/aleo/encryptor.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from ._aleolib import PrivateKey, Ciphertext, Field, Network, Identifier, Plaintext, Literal + + +class Encryptor: + @staticmethod + # Encrypt a private key into ciphertext using a secret + def encrypt_private_key_with_secret(private_key: PrivateKey, secret: str) -> Ciphertext: + seed = private_key.seed() + return Encryptor.__encrypt_field(seed, secret, "private_key") + + @staticmethod + # Decrypt a private key from ciphertext using a secret + def decrypt_private_key_with_secret(ciphertext: Ciphertext, secret: str) -> PrivateKey: + seed = Encryptor.__decrypt_field(ciphertext, secret, "private_key") + return PrivateKey.from_seed(seed) + + @staticmethod + # Encrypted a field element into a ciphertext representation + def __encrypt_field(field: Field, secret: str, domain: str) -> Ciphertext: + domain_f = Field.domain_separator(domain) + secret_f = Field.domain_separator(secret) + + nonce = Field.random() + blinding = Network.hash_psd2([domain_f, nonce, secret_f]) + key = blinding * field + key_kv = (Identifier.from_string("key"), + Plaintext.new_literal(Literal.from_field(key))) + nonce_kv = (Identifier.from_string("nonce"), + Plaintext.new_literal(Literal.from_field(nonce))) + plaintext = Plaintext.new_struct([key_kv, nonce_kv]) + return plaintext.encrypt_symmetric(secret_f) + + @staticmethod + def __extract_value(plaintext: Plaintext, identifier: str) -> Field: + assert plaintext.is_struct() + ident = Identifier.from_string(identifier) + dec_map = plaintext.as_struct() + val = dec_map[ident] + assert val.is_literal() + literal = val.as_literal() + assert literal.type_name() == 'field' + return Field.from_string(str(literal)) + + @staticmethod + # Recover a field element encrypted within ciphertext + def __decrypt_field(ciphertext: Ciphertext, secret: str, domain: str) -> Field: + domain_f = Field.domain_separator(domain) + secret_f = Field.domain_separator(secret) + decrypted = ciphertext.decrypt_symmetric(secret_f) + assert decrypted.is_struct() + recovered_key = Encryptor.__extract_value(decrypted, "key") + recovered_nonce = Encryptor.__extract_value(decrypted, "nonce") + recovered_blinding = Network.hash_psd2([domain_f, recovered_nonce, secret_f]) + return recovered_key / recovered_blinding + diff --git a/sdk/python/aleo/py.typed b/sdk/python/aleo/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/sdk/test.py b/sdk/python/test.py similarity index 88% rename from sdk/test.py rename to sdk/python/test.py index 3f6a757..8d22819 100644 --- a/sdk/test.py +++ b/sdk/python/test.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -import aleo import unittest - +import aleo class TestAleo(unittest.TestCase): @@ -66,6 +65,20 @@ def test_account_sanity(self): self.assertFalse(account.verify(signature, bad_message)) self.assertTrue(signature.verify(account.address(), message)) + def test_encrypt_decrypt_sk(self): + private_key = aleo.PrivateKey.from_string( + "APrivateKey1zkpJYx2NZeJYB74JHpzvQGpKneTP75Dk8dao6paugZXtCz3") + ciphertext = aleo.Ciphertext.from_string( + "ciphertext1qvqt0sp0pp49gjeh50alfalt7ug3g8y7ha6cl3jkavcsnz8d0y9jwr27taxfrwd5kly8lah53qure3vxav6zxr7txattdvscv0kf3vcuqv9cmzj32znx4uwxdawcj3273zhgm8qwpxqczlctuvjvc596mgsqjxwz37f") + recovered = aleo.Encryptor.decrypt_private_key_with_secret(ciphertext, "qwe123") + + self.assertEqual(private_key, recovered) + + encrypted = aleo.Encryptor.encrypt_private_key_with_secret(private_key, "asd123") + other_recovered = aleo.Encryptor.decrypt_private_key_with_secret(encrypted, "asd123") + + self.assertEqual(private_key, other_recovered) + def test_coinbase(self): address = aleo.Address.from_string( "aleo16xwtrvntrfnan84sy3qg2gdkkp5u5p7sjc882lx8n06fjx2k0yqsklw8sv") diff --git a/sdk/src/account/mod.rs b/sdk/src/account/mod.rs index 0fcc3f8..5a21636 100644 --- a/sdk/src/account/mod.rs +++ b/sdk/src/account/mod.rs @@ -29,6 +29,9 @@ pub use record::{RecordCiphertext, RecordPlaintext}; mod signature; pub use signature::Signature; +mod text; +pub use text::{Ciphertext, Plaintext}; + mod view_key; pub use view_key::ViewKey; diff --git a/sdk/src/account/private_key.rs b/sdk/src/account/private_key.rs index cc72486..697f002 100644 --- a/sdk/src/account/private_key.rs +++ b/sdk/src/account/private_key.rs @@ -55,6 +55,12 @@ impl PrivateKey { ComputeKeyNative::try_from(&self.0).unwrap().into() } + /// Returns the account private key from an account seed. + #[staticmethod] + fn from_seed(seed: Field) -> anyhow::Result { + PrivateKeyNative::try_from(seed.into()).map(Self) + } + /// Reads in an account private key from a base58 string. #[staticmethod] fn from_string(private_key: &str) -> anyhow::Result { diff --git a/sdk/src/account/text.rs b/sdk/src/account/text.rs new file mode 100644 index 0000000..f4e2f3c --- /dev/null +++ b/sdk/src/account/text.rs @@ -0,0 +1,200 @@ +// Copyright (C) 2019-2023 Aleo Systems Inc. +// This file is part of the Aleo SDK library. + +// The Aleo SDK library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Aleo SDK library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Aleo SDK library. If not, see . + +use crate::{ + types::{CiphertextNative, LiteralNative, PlaintextNative}, + Address, Field, Group, Identifier, Literal, Scalar, ViewKey, +}; +use std::ops::Deref; + +use once_cell::sync::OnceCell; +use pyo3::{exceptions::PyTypeError, prelude::*}; + +use std::{collections::HashMap, str::FromStr}; + +/// The Aleo ciphertext type. +#[pyclass(frozen)] +pub struct Ciphertext(CiphertextNative); + +#[pymethods] +impl Ciphertext { + /// Reads in the ciphertext string. + #[staticmethod] + fn from_string(s: &str) -> anyhow::Result { + CiphertextNative::from_str(s).map(Self) + } + + /// Decrypts self into plaintext using the given account view key & nonce. + pub fn decrypt(&self, view_key: ViewKey, nonce: Group) -> anyhow::Result { + self.0 + .decrypt(view_key.into(), nonce.into()) + .map(Into::into) + } + + /// Decrypts self into plaintext using the given plaintext view key. + pub fn decrypt_symmetric(&self, plaintext_view_key: Field) -> anyhow::Result<Plaintext> { + self.0 + .decrypt_symmetric(plaintext_view_key.into()) + .map(Into::into) + } + + /// Returns the ciphertext as a string. + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __eq__(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Deref for Ciphertext { + type Target = CiphertextNative; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<CiphertextNative> for Ciphertext { + fn from(value: CiphertextNative) -> Self { + Self(value) + } +} + +/// The Aleo plaintext type. +#[pyclass(frozen)] +#[derive(Clone)] +pub struct Plaintext(PlaintextNative); + +#[pymethods] +impl Plaintext { + /// Returns a plaintext from a string literal. + #[staticmethod] + fn from_string(s: &str) -> anyhow::Result<Self> { + PlaintextNative::from_str(s).map(Self) + } + + /// Returns a new Plaintext from a Literal. + #[staticmethod] + fn new_literal(literal: Literal) -> Self { + PlaintextNative::from(LiteralNative::from(literal)).into() + } + + /// Returns a new Plaintext::Struct from a list of (key, value). + #[staticmethod] + fn new_struct(kv: Vec<(Identifier, Plaintext)>) -> Self { + let kv: Vec<_> = kv.into_iter().map(|(k, v)| (k.into(), v.into())).collect(); + PlaintextNative::Struct(indexmap::IndexMap::from_iter(kv), OnceCell::new()).into() + } + + /// Returns a new Plaintext::Array from a list of values. + #[staticmethod] + fn new_array(values: Vec<Plaintext>) -> Self { + let values: Vec<_> = values.into_iter().map(Into::into).collect(); + PlaintextNative::Array(values, OnceCell::new()).into() + } + + /// Encrypts self to the given address under the given randomizer. + fn encrypt(&self, address: Address, randomizer: Scalar) -> anyhow::Result<Ciphertext> { + self.0.encrypt(&address, randomizer.into()).map(Into::into) + } + + /// Encrypts self under the given plaintext view key. + fn encrypt_symmetric(&self, plaintext_view_key: Field) -> anyhow::Result<Ciphertext> { + self.0 + .encrypt_symmetric(plaintext_view_key.into()) + .map(Into::into) + } + + /// Returns true if self if Plaintext::Literal. + fn is_literal(&self) -> bool { + matches!(self.0, PlaintextNative::Literal(..)) + } + + /// Returns true if self if Plaintext::Struct. + fn is_struct(&self) -> bool { + matches!(self.0, PlaintextNative::Struct(..)) + } + + /// Returns true if self if Plaintext::Array + fn is_array(&self) -> bool { + matches!(self.0, PlaintextNative::Array(..)) + } + + /// Unboxes the underlying Plaintext::Literal. + fn as_literal(&self) -> PyResult<Literal> { + match &self.0 { + PlaintextNative::Literal(literal, _) => Ok(literal.clone().into()), + _ => Err(PyTypeError::new_err("Plaintext is not a literal")), + } + } + + /// Unboxes the underlying Plaintext::Struct. + fn as_struct(&self) -> PyResult<HashMap<Identifier, Plaintext>> { + match &self.0 { + PlaintextNative::Struct(s, _) => { + let res: HashMap<Identifier, Plaintext> = s + .clone() + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(); + Ok(res) + } + _ => Err(PyTypeError::new_err("Plaintext is not a struct")), + } + } + + /// Unboxes the underlying Plaintext::Array. + fn as_array(&self) -> PyResult<Vec<Plaintext>> { + match &self.0 { + PlaintextNative::Array(s, _) => { + let res: Vec<Plaintext> = s.clone().into_iter().map(|v| v.into()).collect(); + Ok(res) + } + _ => Err(PyTypeError::new_err("Plaintext is not an array")), + } + } + + /// Returns the plaintext as a string. + fn __str__(&self) -> String { + self.0.to_string() + } + + fn __eq__(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Deref for Plaintext { + type Target = PlaintextNative; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<PlaintextNative> for Plaintext { + fn from(value: PlaintextNative) -> Self { + Self(value) + } +} + +impl From<Plaintext> for PlaintextNative { + fn from(value: Plaintext) -> Self { + value.0 + } +} diff --git a/sdk/src/account/view_key.rs b/sdk/src/account/view_key.rs index 8bede52..5c946ac 100644 --- a/sdk/src/account/view_key.rs +++ b/sdk/src/account/view_key.rs @@ -82,3 +82,9 @@ impl From<ViewKeyNative> for ViewKey { Self(value) } } + +impl From<ViewKey> for ViewKeyNative { + fn from(value: ViewKey) -> Self { + value.0 + } +} diff --git a/sdk/src/algebra/field.rs b/sdk/src/algebra/field.rs index e93bf32..218ca87 100644 --- a/sdk/src/algebra/field.rs +++ b/sdk/src/algebra/field.rs @@ -16,7 +16,8 @@ use crate::types::FieldNative; -use pyo3::prelude::*; +use pyo3::{exceptions::PyZeroDivisionError, prelude::*}; +use rand::{distributions::Standard, prelude::*}; use snarkvm::prelude::Zero; use std::{ @@ -39,6 +40,20 @@ impl Field { FieldNative::from_str(s).map(Self) } + /// Generates a new field using a cryptographically secure random number generator + #[staticmethod] + fn random() -> Self { + StdRng::from_entropy() + .sample::<FieldNative, _>(Standard) + .into() + } + + /// Initializes a new field as a domain separator. + #[staticmethod] + fn domain_separator(domain: &str) -> Self { + Self(FieldNative::new_domain_separator(domain)) + } + /// Initializes a new field from a `u128`. #[staticmethod] fn from_u128(value: u128) -> Self { @@ -56,6 +71,18 @@ impl Field { self.0.to_string() } + fn __mul__(&self, other: Self) -> Self { + Self(self.0 * other.0) + } + + fn __truediv__(&self, other: Self) -> PyResult<Self> { + if other.is_zero() { + Err(PyZeroDivisionError::new_err("division by zero")) + } else { + Ok(Self(self.0 / other.0)) + } + } + fn __eq__(&self, other: &Self) -> bool { self.0 == other.0 } diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index d708182..8a31937 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -20,6 +20,7 @@ mod account; mod algebra; mod coinbase; mod credits; +mod network; mod programs; mod types; @@ -27,18 +28,20 @@ use account::*; use algebra::*; use coinbase::*; use credits::*; +use network::*; use programs::*; /// The Aleo Python SDK provides a set of libraries aimed at empowering /// Python developers with zk (zero-knowledge) programming capabilities /// via the usage of Aleo's zkSnarks. #[pymodule] -#[pyo3(name = "aleo")] +#[pyo3(name = "_aleolib")] fn register_module(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Account>()?; m.add_class::<Address>()?; m.add_class::<Authorization>()?; m.add_class::<Boolean>()?; + m.add_class::<Ciphertext>()?; m.add_class::<CoinbasePuzzle>()?; m.add_class::<CoinbaseVerifyingKey>()?; m.add_class::<ComputeKey>()?; @@ -57,6 +60,8 @@ fn register_module(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<Literal>()?; m.add_class::<Locator>()?; m.add_class::<MicroCredits>()?; + m.add_class::<Network>()?; + m.add_class::<Plaintext>()?; m.add_class::<PrivateKey>()?; m.add_class::<Process>()?; m.add_class::<Program>()?; diff --git a/sdk/src/network/mod.rs b/sdk/src/network/mod.rs new file mode 100644 index 0000000..caa6a2c --- /dev/null +++ b/sdk/src/network/mod.rs @@ -0,0 +1,54 @@ +// Copyright (C) 2019-2023 Aleo Systems Inc. +// This file is part of the Aleo SDK library. + +// The Aleo SDK library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Aleo SDK library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>. + +use crate::{types::CurrentNetwork, Field}; + +use pyo3::prelude::*; + +use snarkvm::prelude::Network as NetworkTrait; + +/// The type represents the current network. +#[pyclass(frozen)] +#[derive(Clone)] +pub struct Network; + +#[pymethods] +impl Network { + /// The network name. + #[staticmethod] + fn name() -> &'static str { + CurrentNetwork::NAME + } + + /// The network ID. + #[staticmethod] + fn id() -> u16 { + CurrentNetwork::ID + } + + /// The network version. + #[staticmethod] + fn edition() -> u16 { + CurrentNetwork::EDITION + } + + /// Returns the Poseidon hash with an input rate of 2. + #[staticmethod] + fn hash_psd2(input: Vec<Field>) -> anyhow::Result<Field> { + let input: Vec<_> = input.into_iter().map(Into::into).collect(); + CurrentNetwork::hash_psd2(&input).map(Into::into) + } +} diff --git a/sdk/src/programs/identifier.rs b/sdk/src/programs/identifier.rs index 9de0b56..87e9ddc 100644 --- a/sdk/src/programs/identifier.rs +++ b/sdk/src/programs/identifier.rs @@ -27,7 +27,7 @@ use std::{ /// The Aleo identifier type. #[pyclass(frozen)] -#[derive(Clone)] +#[derive(Clone, Eq, Hash, PartialEq)] pub struct Identifier(IdentifierNative); #[pymethods] diff --git a/sdk/src/programs/literal.rs b/sdk/src/programs/literal.rs index 88683dd..d1462df 100644 --- a/sdk/src/programs/literal.rs +++ b/sdk/src/programs/literal.rs @@ -141,6 +141,12 @@ impl Literal { } } +impl From<LiteralNative> for Literal { + fn from(value: LiteralNative) -> Self { + Self(value) + } +} + impl From<Literal> for LiteralNative { fn from(value: Literal) -> Self { value.0