diff --git a/contrib/mir/src/ast.rs b/contrib/mir/src/ast.rs index cacda66d94334dfbdd906576680b748da8f847c0..52159c51db81cbd2eb0e25c85421777608088f5c 100644 --- a/contrib/mir/src/ast.rs +++ b/contrib/mir/src/ast.rs @@ -13,6 +13,7 @@ pub mod parsed; pub mod typechecked; use std::collections::BTreeMap; +pub use tezos_crypto_rs::hash::ChainId; pub use michelson_address::*; pub use michelson_list::MichelsonList; @@ -36,6 +37,7 @@ pub enum Type { Or(Box<(Type, Type)>), Contract(Box), Address, + ChainId, } impl Type { @@ -44,7 +46,7 @@ impl Type { pub fn size_for_gas(&self) -> usize { use Type::*; match self { - Nat | Int | Bool | Mutez | String | Unit | Operation | Address => 1, + Nat | Int | Bool | Mutez | String | Unit | Operation | Address | ChainId => 1, Pair(p) | Or(p) | Map(p) => 1 + p.0.size_for_gas() + p.1.size_for_gas(), Option(x) | List(x) | Contract(x) => 1 + x.size_for_gas(), } @@ -159,6 +161,7 @@ pub enum TypedValue { Map(BTreeMap), Or(Box>), Address(Address), + ChainId(ChainId), } pub fn typed_value_to_value_optimized(tv: TypedValue) -> Value { @@ -193,6 +196,7 @@ pub fn typed_value_to_value_optimized(tv: TypedValue) -> Value { TV::Option(Some(r)) => V::new_option(Some(typed_value_to_value_optimized(*r))), TV::Or(x) => V::new_or(x.map(typed_value_to_value_optimized)), TV::Address(x) => V::Bytes(x.to_bytes_vec()), + TV::ChainId(x) => V::Bytes(x.into()), } } @@ -274,6 +278,7 @@ pub enum Instruction { IfCons(Vec>, Vec>), Iter(T::IterOverload, Vec>), IfLeft(Vec>, Vec>), + ChainId, } pub type ParsedAST = Vec; diff --git a/contrib/mir/src/ast/comparable.rs b/contrib/mir/src/ast/comparable.rs index 51c974f10c3ecfddf4834638f6e7872922024f76..b455fc741d4193930bde02a2489b645400b7caeb 100644 --- a/contrib/mir/src/ast/comparable.rs +++ b/contrib/mir/src/ast/comparable.rs @@ -14,6 +14,7 @@ impl PartialOrd for TypedValue { (Option(x), Option(y)) => x.as_deref().partial_cmp(&y.as_deref()), (Or(x), Or(y)) => x.as_ref().partial_cmp(y.as_ref()), (Address(l), Address(r)) => l.partial_cmp(r), + (ChainId(l), ChainId(r)) => l.partial_cmp(r), _ => None, } } @@ -28,6 +29,8 @@ impl Ord for TypedValue { #[cfg(test)] mod tests { + use tezos_crypto_rs::hash::HashTrait; + use super::*; #[test] @@ -132,6 +135,29 @@ mod tests { } } + #[test] + /// checks that an array of chain ids is sorted without a priori assuming + /// that the comparison operator on chain ids is transitive. + fn compare_chain_ids() { + // ordering was verified against octez-client + let ordered_chain_ids = [ + "00000000", "00000001", "00000002", "00000100", "00000200", "01020304", "a0b0c0d0", + "a1b2c3d4", "ffffffff", + ] + .map(|x| { + TypedValue::ChainId( + tezos_crypto_rs::hash::ChainId::try_from_bytes(&hex::decode(x).unwrap()).unwrap(), + ) + }); + + for (i, addr_i) in ordered_chain_ids.iter().enumerate() { + for (j, addr_j) in ordered_chain_ids.iter().enumerate() { + assert_eq!(addr_i.partial_cmp(addr_j), i.partial_cmp(&j)); + assert_eq!(addr_i.cmp(addr_j), i.cmp(&j)); + } + } + } + #[test] #[should_panic(expected = "Comparing incomparable values in TypedValue")] fn compare_different_comparable() { diff --git a/contrib/mir/src/context.rs b/contrib/mir/src/context.rs index cc2c48adef6900200226d85575438535618935d1..1177d7012e421be59783fd05226a3502fcf8fbd4 100644 --- a/contrib/mir/src/context.rs +++ b/contrib/mir/src/context.rs @@ -1,5 +1,19 @@ -#[derive(Debug, Default)] +use crate::gas::Gas; + +#[derive(Debug)] pub struct Ctx { - pub gas: crate::gas::Gas, + pub gas: Gas, pub amount: i64, + pub chain_id: tezos_crypto_rs::hash::ChainId, +} + +impl Default for Ctx { + fn default() -> Self { + Ctx { + gas: Gas::default(), + amount: 0, + // the default chain id is NetXynUjJNZm7wi, which is also the default chain id of octez-client in mockup mode + chain_id: tezos_crypto_rs::hash::ChainId(vec![0xf3, 0xd4, 0x85, 0x54]), + } + } } diff --git a/contrib/mir/src/gas.rs b/contrib/mir/src/gas.rs index fce194ba98c2e57c3d0c7c0246df42efc8b1a7aa..bbccd5f1f7714dd65cabdd8d0aa806e35ad39eaf 100644 --- a/contrib/mir/src/gas.rs +++ b/contrib/mir/src/gas.rs @@ -107,6 +107,12 @@ pub mod tc_cost { // `max(bls,ed25519,p256,secp256k1)`, which happens to be `bls` pub const KEY_HASH_OPTIMIZED: u32 = 80; + // corresponds to cost_B58CHECK_DECODING_CHAIN_ID in the protocol + pub const CHAIN_ID_READABLE: u32 = 1600; + + // corresponds to cost_DECODING_CHAIN_ID in the protocol + pub const CHAIN_ID_OPTIMIZED: u32 = 50; + fn variadic(depth: u16) -> Result { let depth = Checked::from(depth as u32); (depth * 50).as_gas_cost() @@ -171,6 +177,7 @@ pub mod interpret_cost { pub const AMOUNT: u32 = 10; pub const NIL: u32 = 10; pub const CONS: u32 = 15; + pub const CHAIN_ID: u32 = 15; pub const INTERPRET_RET: u32 = 15; // corresponds to KNil in the Tezos protocol pub const LOOP_ENTER: u32 = 10; // corresponds to KLoop_in in the Tezos protocol @@ -237,6 +244,7 @@ pub mod interpret_cost { }; let cmp_option = Checked::from(10u32); const ADDRESS_SIZE: usize = 20 + 31; // hash size + max entrypoint size + const CMP_CHAIN_ID: u32 = 30; Ok(match (v1, v2) { (V::Nat(l), V::Nat(r)) => { // NB: eventually when using BigInts, use BigInt::bits() &c @@ -259,6 +267,7 @@ pub mod interpret_cost { } .as_gas_cost()?, (V::Address(..), V::Address(..)) => cmp_bytes(ADDRESS_SIZE, ADDRESS_SIZE)?, + (V::ChainId(..), V::ChainId(..)) => CMP_CHAIN_ID, _ => unreachable!("Comparison of incomparable values"), }) } diff --git a/contrib/mir/src/interpreter.rs b/contrib/mir/src/interpreter.rs index fda4656d19514a23b9c9c8f9262504b48511c92b..64f337c98bf75f904006f430d4423db7fca238d3 100644 --- a/contrib/mir/src/interpreter.rs +++ b/contrib/mir/src/interpreter.rs @@ -356,6 +356,10 @@ fn interpret_one( stack.push(V::Map(map)); } }, + I::ChainId => { + ctx.gas.consume(interpret_cost::CHAIN_ID)?; + stack.push(V::ChainId(ctx.chain_id.clone())); + } I::Seq(nested) => interpret(nested, ctx, stack)?, } Ok(()) @@ -1285,4 +1289,21 @@ mod interpreter_tests { ) .unwrap(); // panics } + + #[test] + fn chain_id_instr() { + let chain_id = super::ChainId::from_base58_check("NetXynUjJNZm7wi").unwrap(); + let ctx = &mut Ctx { + chain_id: chain_id.clone(), + ..Ctx::default() + }; + let start_milligas = ctx.gas.milligas(); + let stk = &mut stk![]; + assert_eq!(interpret(&vec![Instruction::ChainId], ctx, stk), Ok(())); + assert_eq!(stk, &stk![TypedValue::ChainId(chain_id)]); + assert_eq!( + start_milligas - ctx.gas.milligas(), + interpret_cost::CHAIN_ID + interpret_cost::INTERPRET_RET + ); + } } diff --git a/contrib/mir/src/lexer.rs b/contrib/mir/src/lexer.rs index 26f7a0ee7f1b791297f493bf8094079d2b567d25..3b480adf10f5a102f76ed1a7477c8b95c3bd20ee 100644 --- a/contrib/mir/src/lexer.rs +++ b/contrib/mir/src/lexer.rs @@ -96,6 +96,8 @@ defprim! { IF_LEFT, contract, address, + chain_id, + CHAIN_ID, } defprim! { diff --git a/contrib/mir/src/syntax.lalrpop b/contrib/mir/src/syntax.lalrpop index 9c7a066f7fad502c381827ba8e74d1eb65663cc0..1cc84f0f270e326f17baeaf4dc147e77252a189e 100644 --- a/contrib/mir/src/syntax.lalrpop +++ b/contrib/mir/src/syntax.lalrpop @@ -53,6 +53,7 @@ extern { "or" => Tok::Prim(PT::Prim(Prim::or)), "contract" => Tok::Prim(PT::Prim(Prim::contract)), "address" => Tok::Prim(PT::Prim(Prim::address)), + "chain_id" => Tok::Prim(PT::Prim(Prim::chain_id)), "True" => Tok::Prim(PT::Prim(Prim::True)), "False" => Tok::Prim(PT::Prim(Prim::False)), "Unit" => Tok::Prim(PT::Prim(Prim::Unit)), @@ -89,6 +90,7 @@ extern { "IF_CONS" => Tok::Prim(PT::Prim(Prim::IF_CONS)), "ITER" => Tok::Prim(PT::Prim(Prim::ITER)), "IF_LEFT" => Tok::Prim(PT::Prim(Prim::IF_LEFT)), + "CHAIN_ID" => Tok::Prim(PT::Prim(Prim::CHAIN_ID)), "(" => Tok::LParen, ")" => Tok::RParen, "{" => Tok::LBrace, @@ -111,6 +113,7 @@ atomic_type: Type = { "unit" => Type::Unit, "operation" => Type::Operation, "address" => Type::Address, + "chain_id" => Type::ChainId, } pair_args: Type = { @@ -196,6 +199,7 @@ atomic_instruction: ParsedInstruction = { "DUP" => Dup(None), "UNPAIR" => Unpair, "CONS" => Cons, + "CHAIN_ID" => Instruction::ChainId, } instruction: ParsedInstruction = { @@ -292,7 +296,8 @@ tztEntity : TztEntity = { "output" "(" "generalOverflow" ")" => Output(TztError(InterpreterError(GeneralOverflow(a1, a2)))), "output" "(" "StaticError" ")" => Output(TztError(TypecheckerError(Some(s)))), "output" "(" "StaticError" "_" ")" => Output(TztError(TypecheckerError(None))), - "amount" => TztEntity::Amount(m) + "amount" => TztEntity::Amount(m), + "chain_id" => TztEntity::ChainId(<>), } pub tztTestEntities : Vec = semicolonSepSeq; diff --git a/contrib/mir/src/typechecker.rs b/contrib/mir/src/typechecker.rs index afa6858ef98a0e282256e6ac1d38948eb649ae87..16a60f707940ac132be8230b439788c1df10da1b 100644 --- a/contrib/mir/src/typechecker.rs +++ b/contrib/mir/src/typechecker.rs @@ -7,6 +7,7 @@ use std::collections::BTreeMap; use std::num::TryFromIntError; +use tezos_crypto_rs::{base58::FromBase58CheckError, hash::FromBytesError}; pub mod type_props; @@ -53,6 +54,28 @@ pub enum TcError { }, #[error(transparent)] AddressError(#[from] AddressError), + #[error("invalid value for chain_id: {0}")] + ChainIdError(#[from] ChainIdError), +} + +#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] +pub enum ChainIdError { + #[error("{0}")] + FromBase58CheckError(String), + #[error("{0}")] + FromBytesError(String), +} + +impl From for ChainIdError { + fn from(value: FromBase58CheckError) -> Self { + Self::FromBase58CheckError(value.to_string()) + } +} + +impl From for ChainIdError { + fn from(value: FromBytesError) -> Self { + Self::FromBytesError(value.to_string()) + } } #[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)] @@ -143,7 +166,7 @@ fn verify_ty(ctx: &mut Ctx, t: &Type) -> Result<(), TcError> { use Type::*; ctx.gas.consume(gas::tc_cost::VERIFY_TYPE_STEP)?; match t { - Nat | Int | Bool | Mutez | String | Operation | Unit | Address => Ok(()), + Nat | Int | Bool | Mutez | String | Operation | Unit | Address | ChainId => Ok(()), Pair(tys) | Or(tys) => { verify_ty(ctx, &tys.0)?; verify_ty(ctx, &tys.1) @@ -562,6 +585,11 @@ fn typecheck_instruction( (I::Update(..), [.., _, _, _]) => no_overload!(UPDATE), (I::Update(..), [] | [_] | [_, _]) => no_overload!(UPDATE, len 3), + (I::ChainId, ..) => { + stack.push(T::ChainId); + I::ChainId + } + (I::Seq(nested), ..) => I::Seq(typecheck(nested, ctx, opt_stack)?), }) } @@ -655,6 +683,17 @@ fn typecheck_value(ctx: &mut Ctx, t: &Type, v: Value) -> Result { + ctx.gas.consume(gas::tc_cost::CHAIN_ID_READABLE)?; + TV::ChainId( + ChainId::from_base58_check(&str).map_err(|x| TcError::ChainIdError(x.into()))?, + ) + } + (T::ChainId, V::Bytes(bs)) => { + use tezos_crypto_rs::hash::HashTrait; + ctx.gas.consume(gas::tc_cost::CHAIN_ID_OPTIMIZED)?; + TV::ChainId(ChainId::try_from_bytes(&bs).map_err(|x| TcError::ChainIdError(x.into()))?) + } (t, v) => return Err(TcError::InvalidValueForType(v, t.clone())), }) } @@ -2483,4 +2522,60 @@ mod typecheck_tests { Err(TcError::AddressError(AddressError::WrongFormat(_))) ); } + + #[test] + fn test_push_chain_id() { + let bytes = "f3d48554"; + let exp = hex::decode(bytes).unwrap(); + let exp = Ok(Push(TypedValue::ChainId(super::ChainId(exp)))); + let lit = "NetXynUjJNZm7wi"; + assert_eq!( + &typecheck_instruction( + parse(&format!("PUSH chain_id \"{}\"", lit)).unwrap(), + &mut Ctx::default(), + &mut tc_stk![], + ), + &exp + ); + assert_eq!( + &typecheck_instruction( + parse(&format!("PUSH chain_id 0x{}", bytes)).unwrap(), + &mut Ctx::default(), + &mut tc_stk![], + ), + &exp + ); + assert_eq!( + typecheck_instruction( + parse("PUSH chain_id \"foobar\"").unwrap(), + &mut Ctx::default(), + &mut tc_stk![], + ), + Err(TcError::ChainIdError( + tezos_crypto_rs::base58::FromBase58CheckError::InvalidChecksum.into() + )) + ); + assert_eq!( + typecheck_instruction( + parse("PUSH chain_id 0xbeef").unwrap(), + &mut Ctx::default(), + &mut tc_stk![], + ), + Err(TcError::ChainIdError( + tezos_crypto_rs::hash::FromBytesError::InvalidSize.into() + )) + ); + } + + #[test] + fn chain_id_instr() { + assert_eq!( + typecheck_instruction( + parse("CHAIN_ID").unwrap(), + &mut Ctx::default(), + &mut tc_stk![] + ), + Ok(Instruction::ChainId) + ); + } } diff --git a/contrib/mir/src/typechecker/type_props.rs b/contrib/mir/src/typechecker/type_props.rs index ff1a0297c954476e178f681dcbef4802fcf2fa29..3004bc9ad82ff6b66e19bf2404ba143e78dff297 100644 --- a/contrib/mir/src/typechecker/type_props.rs +++ b/contrib/mir/src/typechecker/type_props.rs @@ -41,7 +41,7 @@ impl Type { gas.consume(tc_cost::TYPE_PROP_STEP)?; let invalid_type_prop = || Err(TcError::InvalidTypeProperty(prop, self.clone())); match self { - Nat | Int | Bool | Mutez | String | Unit | Address => (), + Nat | Int | Bool | Mutez | String | Unit | Address | ChainId => (), Operation => match prop { TypeProperty::Comparable | TypeProperty::Passable diff --git a/contrib/mir/src/tzt.rs b/contrib/mir/src/tzt.rs index b2ec23267b41a42d9d83b5d530df62cf8af1a4bf..9e4311516901e4f6b931d78e144b23c0e70fc65e 100644 --- a/contrib/mir/src/tzt.rs +++ b/contrib/mir/src/tzt.rs @@ -5,7 +5,6 @@ /* */ /******************************************************************************/ -mod context; mod expectation; use std::fmt; @@ -13,16 +12,16 @@ use std::fmt; use crate::ast::*; use crate::context::*; use crate::interpreter::*; +use crate::irrefutable_match::irrefutable_match; use crate::parser::spanned_lexer; use crate::stack::*; use crate::syntax::tztTestEntitiesParser; use crate::typechecker::*; -use crate::tzt::context::*; use crate::tzt::expectation::*; pub type TestStack = Vec<(Type, TypedValue)>; -#[derive(PartialEq, Eq, Clone)] +#[derive(PartialEq, Eq, Clone, Debug)] pub enum TztTestError { StackMismatch( (FailingTypeStack, Stack), @@ -68,6 +67,7 @@ pub struct TztTest { pub input: TestStack, pub output: TestExpectation, pub amount: Option, + pub chain_id: Option, } fn typecheck_stack(stk: Vec<(Type, Value)>) -> Result, TcError> { @@ -108,6 +108,7 @@ impl TryFrom> for TztTest { let mut m_input: Option = None; let mut m_output: Option = None; let mut m_amount: Option = None; + let mut m_chain_id: Option = None; for e in tzt { match e { @@ -125,6 +126,7 @@ impl TryFrom> for TztTest { }, )?, Amount(m) => set_tzt_field("amount", &mut m_amount, m)?, + ChainId(id) => set_tzt_field("chain_id", &mut m_chain_id, id)?, } } @@ -133,6 +135,14 @@ impl TryFrom> for TztTest { input: m_input.ok_or("input section not found in test")?, output: m_output.ok_or("output section not found in test")?, amount: m_amount, + chain_id: m_chain_id + .map(|v| { + Ok::<_, TcError>(irrefutable_match!( + v.typecheck(&mut Ctx::default(), &Type::ChainId)?; + TypedValue::ChainId + )) + }) + .transpose()?, }) } } @@ -197,6 +207,7 @@ pub enum TztEntity { Input(Vec<(Type, Value)>), Output(TztOutput), Amount(i64), + ChainId(Value), } /// Possible values for the "output" expectation field in a Tzt test @@ -232,7 +243,11 @@ pub fn run_tzt_test(test: TztTest) -> Result<(), TztTestError> { // Here we compare the outcome of the interpreting with the // expectation from the test, and declare the result of the test // accordingly. - let mut ctx = construct_context(&test); + let mut ctx = Ctx { + gas: crate::gas::Gas::default(), + amount: test.amount.unwrap_or_default(), + chain_id: test.chain_id.unwrap_or(Ctx::default().chain_id), + }; let execution_result = execute_tzt_test_code(test.code, &mut ctx, test.input); check_expectation(&mut ctx, test.output, execution_result) } diff --git a/contrib/mir/src/tzt/context.rs b/contrib/mir/src/tzt/context.rs deleted file mode 100644 index 515925c7e78a703f383c3873ec92503b434e8829..0000000000000000000000000000000000000000 --- a/contrib/mir/src/tzt/context.rs +++ /dev/null @@ -1,16 +0,0 @@ -/******************************************************************************/ -/* */ -/* SPDX-License-Identifier: MIT */ -/* Copyright (c) [2023] Serokell */ -/* */ -/******************************************************************************/ - -use crate::context::*; -use crate::tzt::*; - -pub fn construct_context(test: &TztTest) -> Ctx { - Ctx { - gas: crate::gas::Gas::default(), - amount: test.amount.unwrap_or_default(), - } -} diff --git a/contrib/mir/tzt_runner/main.rs b/contrib/mir/tzt_runner/main.rs index f52921437870e565b3538d62bb5f48dd70ad927a..8d795c2eda0ad9a38e40dd2b93d45d233bf7a5a0 100644 --- a/contrib/mir/tzt_runner/main.rs +++ b/contrib/mir/tzt_runner/main.rs @@ -136,6 +136,34 @@ mod tztrunner_tests { )); } + #[test] + fn test_runner_chain_id() { + assert_eq!( + run_tzt_test( + parse_tzt_test( + r#"code { CHAIN_ID }; + input {}; + chain_id "NetXdQprcVkpaWU"; + output { Stack_elt chain_id 0x7a06a770 }"#, + ) + .unwrap() + ), + Ok(()) + ); + assert_eq!( + run_tzt_test( + parse_tzt_test( + r#"code { CHAIN_ID }; + input {}; + chain_id 0xbeeff00d; + output { Stack_elt chain_id 0xbeeff00d }"#, + ) + .unwrap() + ), + Ok(()) + ); + } + const TZT_SAMPLE_ADD: &str = "code { ADD } ; input { Stack_elt int 5 ; Stack_elt int 5 } ; output { Stack_elt int 10 }";