Introduce 'Never' type as a safe alternative to always None options

Unfortunately, as documented in:

  https://github.com/IntersectMBO/cardano-ledger/issues/4571

  Some Option fields in the script context certificates are going to
  remain set to None, at least until the next Hard fork. There's a risk
  that people permanently lock their funds if they expect deposits on
  registration credentials to ever be `Some`.

  So, we introduce a special type that emulate an `Option` that can only
  ever be `None`. We call it `Never` and it is the first type of this
  kind (i.e. with constructors indexes not starting at 0).
This commit is contained in:
KtorZ 2024-08-27 11:06:02 +02:00
parent ff25fbd970
commit d74e36d0bc
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
13 changed files with 269 additions and 68 deletions

View File

@ -400,6 +400,19 @@ impl TypedDataType {
doc: None,
}
}
pub fn is_never(&self) -> bool {
self.name == well_known::NEVER
&& self.constructors.len() == well_known::NEVER_CONSTRUCTORS.len()
&& self.location == Span::empty()
&& self
.constructors
.iter()
.zip(well_known::NEVER_CONSTRUCTORS)
.all(|(constructor, name)| {
name == &constructor.name && constructor.arguments.is_empty()
})
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]

View File

@ -16,6 +16,8 @@ pub const LIST: &str = "List";
pub const MILLER_LOOP_RESULT: &str = "MillerLoopResult";
pub const OPTION: &str = "Option";
pub const OPTION_CONSTRUCTORS: &[&str] = &["Some", "None"];
pub const NEVER: &str = "Never";
pub const NEVER_CONSTRUCTORS: &[&str] = &["__hole", "Never"];
pub const ORDERING: &str = "Ordering";
pub const ORDERING_CONSTRUCTORS: &[&str] = &["Less", "Equal", "Greater"];
pub const PAIR: &str = "Pair";
@ -297,6 +299,17 @@ impl Type {
})
}
pub fn never() -> Rc<Type> {
Rc::new(Type::App {
public: true,
contains_opaque: false,
name: NEVER.to_string(),
module: "".to_string(),
args: vec![],
alias: None,
})
}
pub fn ordering() -> Rc<Type> {
Rc::new(Type::App {
public: true,

View File

@ -168,6 +168,19 @@ pub fn prelude(id_gen: &IdGenerator) -> TypeInfo {
),
);
// Never
prelude.types.insert(
well_known::NEVER.to_string(),
TypeConstructor::primitive(Type::never()),
);
prelude.types_constructors.insert(
well_known::NEVER.to_string(),
ValueConstructor::known_adt(
&mut prelude.values,
&[(well_known::NEVER_CONSTRUCTORS[1], Type::never())],
),
);
// Cardano ScriptContext
prelude.types.insert(
well_known::SCRIPT_CONTEXT.to_string(),
@ -1503,6 +1516,15 @@ pub fn prelude_data_types(id_gen: &IdGenerator) -> IndexMap<DataTypeKey, TypedDa
option_data_type,
);
// Never
data_types.insert(
DataTypeKey {
module_name: "".to_string(),
defined_type: well_known::NEVER.to_string(),
},
TypedDataType::never(),
);
// PRNG
let prng_data_type = TypedDataType::prng();
data_types.insert(
@ -1609,4 +1631,8 @@ impl TypedDataType {
typed_parameters: vec![tipo],
}
}
pub fn never() -> Self {
DataType::known_enum(well_known::NEVER, well_known::NEVER_CONSTRUCTORS)
}
}

View File

@ -1434,7 +1434,9 @@ impl<'a> CodeGenerator<'a> {
.unwrap_or_else(|| unreachable!("Failed to find definition for {}", name));
let then = if props.kind.is_expect()
&& (data_type.constructors.len() > 1 || props.full_check)
&& (data_type.constructors.len() > 1
|| props.full_check
|| data_type.is_never())
{
let (index, _) = data_type
.constructors
@ -1459,8 +1461,8 @@ impl<'a> CodeGenerator<'a> {
)
} else {
assert!(
data_type.constructors.len() == 1,
"attempted expect on a type with more or less than 1 constructor: \nis_expect? {}\nfull_check? {}\ndata_type={data_type:#?}\n{}",
data_type.constructors.len() == 1 || data_type.is_never(),
"attempted let-assignment on a type with more or less than 1 constructor: \nis_expect? {}\nfull_check? {}\ndata_type={data_type:#?}\n{}",
props.kind.is_expect(),
props.full_check,
name,
@ -1978,9 +1980,20 @@ impl<'a> CodeGenerator<'a> {
let otherwise_delayed = AirTree::local_var("otherwise_delayed", Type::void());
let is_never = data_type.is_never();
let constr_clauses = data_type.constructors.iter().enumerate().rfold(
otherwise_delayed.clone(),
|acc, (index, constr)| {
// NOTE: For the Never type, we have an placeholder first constructor
// that must be ignored. The Never type is considered to have only one
// constructor starting at index 1 so it shouldn't be possible to
// cast from Data into the first constructor. There's virtually no
// constructor at index 0.
if is_never && index == 0 {
return acc;
}
let mut constr_args = vec![];
let constr_then = constr.arguments.iter().enumerate().rfold(
@ -2169,7 +2182,7 @@ impl<'a> CodeGenerator<'a> {
),
)
} else if let Some(data_type) = data_type {
if data_type.constructors.len() > 1 {
if data_type.constructors.len() > 1 && !data_type.is_never() {
AirTree::clause(
&props.original_subject_name,
clause_cond,
@ -5584,8 +5597,18 @@ impl<'a> CodeGenerator<'a> {
} => {
let tail_name_prefix = "__tail_index";
let data_type = lookup_data_type_by_tipo(&self.data_types, &tipo)
.unwrap_or_else(|| panic!("HOW DID YOU DO THIS ON BOOL OR VOID"));
let data_type =
lookup_data_type_by_tipo(&self.data_types, &tipo).unwrap_or_else(|| {
panic!(
"Attempted record update on an unknown type!\ntype: {:#?}",
tipo
)
});
assert!(
!data_type.is_never(),
"Attempted record update on a Never type.",
);
let constructor_field_count = data_type.constructors[0].arguments.len();
let record = arg_stack.pop().unwrap();

View File

@ -83,9 +83,11 @@ fn assert_uplc(source_code: &str, expected: Term<Name>, should_fail: bool) {
format!("{:#?}", eval.logs())
);
if !should_fail {
assert_eq!(eval.result().unwrap(), Term::bool(true));
}
assert!(if should_fail {
eval.failed(false)
} else {
!eval.failed(false)
});
}
TestType::Validator(func) => {
let program = generator.generate(func, &script.1);
@ -6137,3 +6139,51 @@ fn pattern_bytearray() {
assert_uplc(src, program, false)
}
#[test]
fn cast_never() {
let src = r#"
test never_ok_cast() {
let none: Option<Void> = None
let data: Data = none
expect _: Never = data
}
"#;
let none_or_never = || Term::Constant(Constant::Data(Data::constr(1, vec![])).into());
let expect_otherwise = Term::Error
.delayed_trace(Term::string("expect _: Never = data"))
.delay();
let assert_constr_index = Term::equals_integer()
.apply(Term::integer(1.into()))
.apply(Term::fst_pair().apply(Term::unconstr_data().apply(none_or_never())));
let assert_empty_fields = |then: Term<Name>, expect_otherwise: Rc<Name>| {
Term::snd_pair()
.apply(Term::unconstr_data().apply(none_or_never()))
.delay_empty_choose_list(then, Term::Var(expect_otherwise))
};
let program = expect_otherwise.as_var("expect_:Never=data", |expect_otherwise| {
let otherwise = Term::Var(expect_otherwise.clone());
let when_constr = assert_constr_index.delay_true_if_then_else(
assert_empty_fields(Term::unit(), expect_otherwise.clone()),
Term::Var(expect_otherwise),
);
none_or_never()
.choose_data(
when_constr.delay(),
otherwise.clone(),
otherwise.clone(),
otherwise.clone(),
otherwise,
)
.force()
});
assert_uplc(src, program, false)
}

View File

@ -570,20 +570,9 @@ Constr(
),
Constr(
Constr {
tag: 121,
tag: 122,
any_constructor: None,
fields: [
BigInt(
Int(
Int(
Int {
neg: false,
val: 3000000,
},
),
),
),
],
fields: [],
},
),
],
@ -638,20 +627,9 @@ Constr(
),
Constr(
Constr {
tag: 121,
tag: 122,
any_constructor: None,
fields: [
BigInt(
Int(
Int(
Int {
neg: false,
val: 3000000,
},
),
),
),
],
fields: [],
},
),
],

View File

@ -52,6 +52,8 @@ struct WithArrayRational<'a, T>(&'a T);
struct WithPartialCertificates<'a, T>(&'a T);
struct WithNeverRegistrationDeposit<'a, T>(&'a T);
pub trait ToPlutusData {
fn to_plutus_data(&self) -> PlutusData;
}
@ -203,6 +205,29 @@ impl<'a> ToPlutusData for WithWrappedTransactionId<'a, KeyValuePairs<ScriptPurpo
}
}
impl<'a> ToPlutusData for WithNeverRegistrationDeposit<'a, Vec<Certificate>> {
fn to_plutus_data(&self) -> PlutusData {
self.0
.iter()
.map(WithNeverRegistrationDeposit)
.collect::<Vec<_>>()
.to_plutus_data()
}
}
impl<'a> ToPlutusData for WithNeverRegistrationDeposit<'a, KeyValuePairs<ScriptPurpose, Redeemer>> {
fn to_plutus_data(&self) -> PlutusData {
let mut data_vec: Vec<(PlutusData, PlutusData)> = vec![];
for (key, value) in self.0.iter() {
data_vec.push((
WithNeverRegistrationDeposit(key).to_plutus_data(),
value.to_plutus_data(),
))
}
PlutusData::Map(KeyValuePairs::Def(data_vec))
}
}
impl<A: ToPlutusData> ToPlutusData for Option<A> {
fn to_plutus_data(&self) -> PlutusData {
match self {
@ -549,14 +574,16 @@ impl<'a> ToPlutusData for WithPartialCertificates<'a, Certificate> {
vec![pool_keyhash.to_plutus_data(), epoch.to_plutus_data()],
),
certificate => unreachable!("unexpected in V1/V2 script context: {certificate:?}"),
certificate => {
unreachable!("unexpected certificate type in V1/V2 script context: {certificate:?}")
}
}
}
}
impl ToPlutusData for Certificate {
impl<'a> ToPlutusData for WithNeverRegistrationDeposit<'a, Certificate> {
fn to_plutus_data(&self) -> PlutusData {
match self {
match self.0 {
Certificate::StakeRegistration(stake_credential) => wrap_multiple_with_constr(
0,
vec![
@ -565,11 +592,11 @@ impl ToPlutusData for Certificate {
],
),
Certificate::Reg(stake_credential, deposit) => wrap_multiple_with_constr(
Certificate::Reg(stake_credential, _) => wrap_multiple_with_constr(
0,
vec![
stake_credential.to_plutus_data(),
Some(*deposit).to_plutus_data(),
None::<PlutusData>.to_plutus_data(),
],
),
@ -581,11 +608,11 @@ impl ToPlutusData for Certificate {
],
),
Certificate::UnReg(stake_credential, deposit) => wrap_multiple_with_constr(
Certificate::UnReg(stake_credential, _) => wrap_multiple_with_constr(
1,
vec![
stake_credential.to_plutus_data(),
Some(*deposit).to_plutus_data(),
None::<PlutusData>.to_plutus_data(),
],
),
@ -835,32 +862,44 @@ impl ToPlutusData for TxInInfo {
}
}
// NOTE: This is a _small_ abuse of the 'WithWrappedTransactionId'. We know the wrapped
// is needed for V1 and V2, and it also appears that for V1 and V2, the certifying
// purpose mustn't include the certificate index. So, we also short-circuit it here.
impl<'a> ToPlutusData for WithWrappedTransactionId<'a, ScriptPurpose> {
fn to_plutus_data(&self) -> PlutusData {
match self.0 {
ScriptPurpose::Minting(policy_id) => wrap_with_constr(0, policy_id.to_plutus_data()),
ScriptPurpose::Spending(out_ref, ()) => {
wrap_with_constr(1, WithWrappedTransactionId(out_ref).to_plutus_data())
}
// NOTE: This is a _small_ abuse of the 'WithWrappedTransactionId'. We know the wrapped
// is needed for V1 and V2, and it also appears that for V1 and V2, the certifying
// purpose mustn't include the certificate index. So, we also short-circuit it here.
ScriptPurpose::Certifying(_, dcert) => wrap_with_constr(3, dcert.to_plutus_data()),
otherwise => otherwise.to_plutus_data(),
ScriptPurpose::Rewarding(stake_credential) => {
wrap_with_constr(2, stake_credential.to_plutus_data())
}
ScriptPurpose::Certifying(_, dcert) => {
wrap_with_constr(3, WithPartialCertificates(dcert).to_plutus_data())
}
purpose => {
unreachable!("unsupported purpose for V1 or V2 script context: {purpose:?}")
}
}
}
}
impl ToPlutusData for ScriptPurpose {
impl<'a> ToPlutusData for WithNeverRegistrationDeposit<'a, ScriptPurpose> {
fn to_plutus_data(&self) -> PlutusData {
match self {
match self.0 {
ScriptPurpose::Minting(policy_id) => wrap_with_constr(0, policy_id.to_plutus_data()),
ScriptPurpose::Spending(out_ref, ()) => wrap_with_constr(1, out_ref.to_plutus_data()),
ScriptPurpose::Rewarding(stake_credential) => {
wrap_with_constr(2, stake_credential.to_plutus_data())
}
ScriptPurpose::Certifying(ix, dcert) => {
wrap_multiple_with_constr(3, vec![ix.to_plutus_data(), dcert.to_plutus_data()])
}
ScriptPurpose::Certifying(ix, dcert) => wrap_multiple_with_constr(
3,
vec![
ix.to_plutus_data(),
WithNeverRegistrationDeposit(dcert).to_plutus_data(),
],
),
ScriptPurpose::Voting(voter) => {
wrap_multiple_with_constr(4, vec![voter.to_plutus_data()])
}
@ -1240,12 +1279,12 @@ impl ToPlutusData for Vote {
}
}
impl<T> ToPlutusData for ScriptInfo<T>
impl<'a, T> ToPlutusData for WithNeverRegistrationDeposit<'a, ScriptInfo<T>>
where
T: ToPlutusData,
{
fn to_plutus_data(&self) -> PlutusData {
match self {
match self.0 {
ScriptInfo::Minting(policy_id) => wrap_with_constr(0, policy_id.to_plutus_data()),
ScriptInfo::Spending(out_ref, datum) => {
wrap_multiple_with_constr(1, vec![out_ref.to_plutus_data(), datum.to_plutus_data()])
@ -1253,9 +1292,13 @@ where
ScriptInfo::Rewarding(stake_credential) => {
wrap_with_constr(2, stake_credential.to_plutus_data())
}
ScriptInfo::Certifying(ix, dcert) => {
wrap_multiple_with_constr(3, vec![ix.to_plutus_data(), dcert.to_plutus_data()])
}
ScriptInfo::Certifying(ix, dcert) => wrap_multiple_with_constr(
3,
vec![
ix.to_plutus_data(),
WithNeverRegistrationDeposit(dcert).to_plutus_data(),
],
),
ScriptInfo::Voting(voter) => wrap_multiple_with_constr(4, vec![voter.to_plutus_data()]),
ScriptInfo::Proposing(ix, procedure) => {
wrap_multiple_with_constr(5, vec![ix.to_plutus_data(), procedure.to_plutus_data()])
@ -1311,11 +1354,11 @@ impl ToPlutusData for TxInfo {
tx_info.outputs.to_plutus_data(),
tx_info.fee.to_plutus_data(),
tx_info.mint.to_plutus_data(),
tx_info.certificates.to_plutus_data(),
WithNeverRegistrationDeposit(&tx_info.certificates).to_plutus_data(),
tx_info.withdrawals.to_plutus_data(),
tx_info.valid_range.to_plutus_data(),
tx_info.signatories.to_plutus_data(),
tx_info.redeemers.to_plutus_data(),
WithNeverRegistrationDeposit(&tx_info.redeemers).to_plutus_data(),
tx_info.data.to_plutus_data(),
tx_info.id.to_plutus_data(),
tx_info.votes.to_plutus_data(),
@ -1347,7 +1390,7 @@ impl ToPlutusData for ScriptContext {
vec![
tx_info.to_plutus_data(),
redeemer.to_plutus_data(),
purpose.to_plutus_data(),
WithNeverRegistrationDeposit(purpose).to_plutus_data(),
],
),
}

View File

@ -0,0 +1,7 @@
# This file was generated by Aiken
# You typically do not need to edit this file
requirements = []
packages = []
[etags]

View File

@ -0,0 +1,9 @@
name = "aiken-lang/acceptance_test_110"
version = "0.0.0"
license = "Apache-2.0"
description = "Aiken contracts for project 'aiken-lang/110'"
[repository]
user = "aiken-lang"
project = "110"
platform = "github"

View File

@ -0,0 +1,39 @@
type Foo {
Foo(Int, Never)
Bar
}
test never_is_none() {
let none: Option<Void> = None
trace @"Never": Never
trace @"None": none
let data_never: Data = Never
let data_none: Data = none
data_never == data_none
}
test never_pattern_match() {
when Foo(14, Never) is {
Foo(x, Never) -> x == 14
Bar -> False
}
}
test never_assignment() {
let Never = Never
True
}
test never_wrong_cast() fail {
let data: Data = Some(42)
expect _: Never = data
}
test never_ok_cast() {
let none: Option<Void> = None
let data: Data = none
expect _: Never = data
}

View File

@ -13,4 +13,4 @@ requirements = []
source = "github"
[etags]
"aiken-lang/stdlib@v2" = [{ secs_since_epoch = 1724491200, nanos_since_epoch = 427525000 }, "cdbbce58b61deb385e7ea787a2e0fc2dc8fe94db9999e0e6275bc9c70e5796be"]
"aiken-lang/stdlib@v2" = [{ secs_since_epoch = 1724760716, nanos_since_epoch = 700202000 }, "cdbbce58b61deb385e7ea787a2e0fc2dc8fe94db9999e0e6275bc9c70e5796be"]

File diff suppressed because one or more lines are too long

View File

@ -71,7 +71,7 @@ validator purposes {
Some(
RegisterCredential {
credential: VerificationKey(only0s),
deposit: Some(3_000_000),
deposit: None,
},
) == list.at(certificates, 5)
@ -79,7 +79,7 @@ validator purposes {
Some(
UnregisterCredential {
credential: VerificationKey(only0s),
refund: Some(3_000_000),
refund: None,
},
) == list.at(certificates, 6)