Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 96 additions & 42 deletions pallas-primitives/src/conway/script_data.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,65 @@
use std::collections::BTreeMap;

use super::{CostModel, PlutusData, Redeemers, WitnessSet};
use pallas_codec::minicbor::{self, Encode};
use pallas_codec::utils::{KeepRaw, NonEmptySet};
use serde::{Deserialize, Serialize};

pub type PlutusVersion = u8;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LanguageView(pub PlutusVersion, pub CostModel);
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct LanguageViews(pub BTreeMap<PlutusVersion, CostModel>);

impl<C> Encode<C> for LanguageView {
impl FromIterator<(PlutusVersion, CostModel)> for LanguageViews {
fn from_iter<I: IntoIterator<Item = (PlutusVersion, CostModel)>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}

impl<C> Encode<C> for LanguageViews {
fn encode<W: minicbor::encode::Write>(
&self,
e: &mut minicbor::Encoder<W>,
ctx: &mut C,
) -> Result<(), minicbor::encode::Error<W::Error>> {
match self.0 {
0 => {
let mut inner = vec![];
let mut sub = minicbor::Encoder::new(&mut inner);

sub.begin_array().unwrap();
for v in self.1.iter() {
sub.encode_with(v, ctx).unwrap();
}
sub.end().unwrap();
let order: Vec<u8> = self.0.keys().copied().collect();
let mut canonical_order: Vec<u8> = order.into_iter().filter(|&k| k != 0).collect();
canonical_order.sort();
// PlutusV1 is CBOR encoded as 0x4100 so it goes last
if self.0.contains_key(&0) {
canonical_order.push(0);
}

e.map(1)?;
e.bytes(&minicbor::to_vec(0).unwrap())?;
e.bytes(&inner)?;
Ok(())
}
_ => {
e.map(1)?;
e.encode(self.0)?;
e.encode(&self.1)?;
Ok(())
e.map(self.0.len() as u64)?;
for lang in canonical_order {
let cost_model = self.0.get(&lang).unwrap();
match lang {
0 => {
let mut inner = vec![];
let mut sub = minicbor::Encoder::new(&mut inner);
sub.begin_array().unwrap();
for v in cost_model.iter() {
sub.encode_with(v, ctx).unwrap();
}
sub.end().unwrap();
e.bytes(&minicbor::to_vec(0).unwrap())?;
e.bytes(&inner)?;
}
_ => {
e.encode(lang)?;
e.encode(cost_model)?;
}
}
}
Ok(())
}
}

#[derive(Debug, Clone)]
pub struct ScriptData<'b> {
pub redeemers: Option<Redeemers>,
pub datums: Option<KeepRaw<'b, NonEmptySet<KeepRaw<'b, PlutusData>>>>,
pub language_view: Option<LanguageView>,
pub language_views: Option<LanguageViews>,
}

impl ScriptData<'_> {
Expand All @@ -61,8 +76,8 @@ impl ScriptData<'_> {
minicbor::encode(datums, &mut buf).unwrap(); // infallible
}

if let Some(language_view) = &self.language_view {
minicbor::encode(language_view, &mut buf).unwrap(); // infallible
if let Some(language_views) = &self.language_views {
minicbor::encode(language_views, &mut buf).unwrap(); // infallible
} else {
buf.push(0xa0);
}
Expand All @@ -74,28 +89,25 @@ impl ScriptData<'_> {
impl<'b> ScriptData<'b> {
pub fn build_for(
witness: &WitnessSet<'b>,
language_view_opt: &Option<LanguageView>,
language_views_opt: &Option<LanguageViews>,
) -> Option<Self> {
let redeemers = witness.redeemer.as_ref().map(|x| x.to_owned().unwrap());
let datums = witness.plutus_data.clone();

// Only return None if both redeemers and datums are None
if redeemers.is_none() && datums.is_none() {
return None;
}

// When redeemers are present, include the language view
// When only datums are present, language view should be None (empty map in hash)
let language_view = if redeemers.is_some() && language_view_opt.is_some() {
language_view_opt.clone()
let language_views = if redeemers.is_some() && language_views_opt.is_some() {
language_views_opt.clone()
} else {
None
};

Some(ScriptData {
redeemers,
datums,
language_view,
language_views,
})
}
}
Expand All @@ -108,7 +120,7 @@ mod tests {

use super::*;

const COST_MODEL_PLUTUS_V1: LazyLock<Vec<i64>> = LazyLock::new(|| {
static COST_MODEL_PLUTUS_V1: LazyLock<Vec<i64>> = LazyLock::new(|| {
vec![
100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, 4, 1, 11183, 32, 201305, 8356, 4,
16000, 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, 100,
Expand Down Expand Up @@ -140,33 +152,75 @@ mod tests {
]
});

const TEST_VECTORS: LazyLock<Vec<(Vec<u8>, Option<LanguageView>)>> = LazyLock::new(|| {
static COST_MODEL_PLUTUS_V3: LazyLock<Vec<i64>> = LazyLock::new(|| {
vec![
100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, 4, 1, 11183, 32, 201305, 8356, 4,
16000, 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, 100,
16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, 178, 0, 1, 22151, 32, 91189, 769,
4, 2, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 1, 1000, 42921, 4, 2,
24548, 29498, 38, 1, 898148, 27279, 1, 51775, 558, 1, 39184, 1000, 60594, 1, 141895,
32, 83150, 32, 15299, 32, 76049, 1, 13169, 4, 22100, 10, 28999, 74, 1, 28999, 74, 1,
43285, 552, 1, 44749, 541, 1, 33852, 32, 68246, 32, 72362, 32, 7243, 32, 7391, 32,
11546, 32, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 90434, 519, 0, 1,
74433, 32, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 1, 85848, 123203,
7305, -900, 1716, 549, 57, 85848, 0, 1, 955506, 213312, 0, 2, 270652, 22588, 4,
1457325, 64566, 4, 20467, 1, 4, 0, 141992, 32, 100788, 420, 1, 1, 81663, 32, 59498, 32,
20142, 32, 24588, 32, 20744, 32, 25933, 32, 24623, 32, 43053543, 10, 53384111, 14333,
10, 43574283, 26308, 10, 16000, 100, 16000, 100, 962335, 18, 2780678, 6, 442008, 1,
52538055, 3756, 18, 267929, 18, 76433006, 8868, 18, 52948122, 18, 1995836, 36, 3227919,
12, 901022, 1, 166917843, 4307, 36, 284546, 36, 158221314, 26549, 36, 74698472, 36,
333849714, 1, 254006273, 72, 2174038, 72, 2261318, 64571, 4, 207616, 8310, 4, 1293828,
28716, 63, 0, 1, 1006041, 43623, 251, 0, 1, 100181, 726, 719, 0, 1, 100181, 726, 719,
0, 1, 100181, 726, 719, 0, 1, 107878, 680, 0, 1, 95336, 1, 281145, 18848, 0, 1, 180194,
159, 1, 1, 158519, 8942, 0, 1, 159378, 8813, 0, 1, 107490, 3298, 1, 106057, 655, 1,
1964219, 24520, 3,
]
});

static TEST_VECTORS: LazyLock<Vec<(Vec<u8>, Option<LanguageViews>)>> = LazyLock::new(|| {
vec![
(
hex::decode(include_str!("../../../test_data/conway1.tx")).unwrap(),
Some(LanguageView(1, COST_MODEL_PLUTUS_V2.clone())),
Some(LanguageViews::from_iter([(
1,
COST_MODEL_PLUTUS_V2.clone(),
)])),
),
(
hex::decode(include_str!("../../../test_data/conway2.tx")).unwrap(),
Some(LanguageView(0, COST_MODEL_PLUTUS_V1.clone())),
Some(LanguageViews::from_iter([(
0,
COST_MODEL_PLUTUS_V1.clone(),
)])),
),
(
hex::decode(include_str!("../../../test_data/hydra-init.tx")).unwrap(),
Some(LanguageView(1, COST_MODEL_PLUTUS_V2.clone())),
Some(LanguageViews::from_iter([(
1,
COST_MODEL_PLUTUS_V2.clone(),
)])),
),
(
hex::decode(include_str!("../../../test_data/datum-only.tx")).unwrap(),
None,
),
(
hex::decode(include_str!("../../../test_data/conway9.tx")).unwrap(),
Some(LanguageViews::from_iter([
(0, COST_MODEL_PLUTUS_V1.clone()),
(1, COST_MODEL_PLUTUS_V2.clone()),
(2, COST_MODEL_PLUTUS_V3.clone()),
])),
),
]
});

fn assert_script_data_hash_matches(bytes: &[u8], language_view_opt: &Option<LanguageView>) {
fn assert_script_data_hash_matches(bytes: &[u8], language_views_opt: &Option<LanguageViews>) {
let tx: Tx = pallas_codec::minicbor::decode(bytes).unwrap();

let witness = tx.transaction_witness_set.clone().unwrap();

let script_data = ScriptData::build_for(&witness, language_view_opt).unwrap();
let script_data = ScriptData::build_for(&witness, language_views_opt).unwrap();

let obtained = script_data.hash();

Expand All @@ -177,8 +231,8 @@ mod tests {

#[test]
fn test_script_data_hash() {
for (bytes, language_view_opt) in TEST_VECTORS.iter() {
assert_script_data_hash_matches(bytes, language_view_opt);
for (bytes, language_views_opt) in TEST_VECTORS.iter() {
assert_script_data_hash_matches(bytes, language_views_opt);
}
}
}
4 changes: 2 additions & 2 deletions pallas-txbuilder/src/conway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,11 @@ impl BuildConway for StagingTransaction {
None
};

let script_data_hash = self.language_view.map(|language_view| {
let script_data_hash = self.language_views.as_ref().map(|language_views| {
let dta = pallas_primitives::conway::ScriptData {
redeemers: Some(witness_set_redeemers.clone()),
datums: witness_set_datums.clone(),
language_view: Some(language_view),
language_views: Some(language_views.clone()),
};

dta.hash()
Expand Down
27 changes: 19 additions & 8 deletions pallas-txbuilder/src/transaction/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub struct StagingTransaction {
pub script_data_hash: Option<Bytes32>,
pub signature_amount_override: Option<u8>,
pub change_address: Option<Address>,
pub language_view: Option<pallas_primitives::conway::LanguageView>,
pub language_views: Option<pallas_primitives::conway::LanguageViews>,
pub auxiliary_data: Option<AuxiliaryData>,
// pub certificates: TODO
// pub withdrawals: TODO
Expand Down Expand Up @@ -288,14 +288,25 @@ impl StagingTransaction {
self
}

pub fn language_view(mut self, plutus_version: ScriptKind, cost_model: Vec<i64>) -> Self {
self.language_view = match plutus_version {
ScriptKind::PlutusV1 => Some(pallas_primitives::conway::LanguageView(0, cost_model)),
ScriptKind::PlutusV2 => Some(pallas_primitives::conway::LanguageView(1, cost_model)),
ScriptKind::PlutusV3 => Some(pallas_primitives::conway::LanguageView(2, cost_model)),
ScriptKind::Native => None,
};
pub fn language_views(mut self, views: pallas_primitives::conway::LanguageViews) -> Self {
self.language_views = Some(views);
self
}

pub fn add_language(mut self, plutus_version: ScriptKind, cost_model: Vec<i64>) -> Self {
let version = match plutus_version {
ScriptKind::PlutusV1 => 0,
ScriptKind::PlutusV2 => 1,
ScriptKind::PlutusV3 => 2,
ScriptKind::Native => return self,
};
let mut map = self
.language_views
.as_ref()
.map(|v| v.0.clone())
.unwrap_or_default();
map.insert(version, cost_model);
self.language_views = Some(pallas_primitives::conway::LanguageViews(map));
self
}

Expand Down
5 changes: 4 additions & 1 deletion pallas-txbuilder/src/transaction/serialise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,10 @@ mod tests {
signature_amount_override: Some(5),
change_address: Some(Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap())),
script_data_hash: Some(Bytes32([0; 32])),
language_view: Some(pallas_primitives::conway::LanguageView(1, vec![1, 2, 3])),
language_views: Some(pallas_primitives::conway::LanguageViews::from_iter([(
1,
vec![1, 2, 3],
)])),
auxiliary_data: None,
};

Expand Down
48 changes: 25 additions & 23 deletions pallas-validate/src/phase1/conway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use pallas_codec::utils::{Bytes, KeepRaw, NonEmptySet};
use pallas_primitives::{
babbage,
conway::{
DatumOption, Language, LanguageView, Mint, Redeemers, RedeemersKey, RequiredSigners,
DatumOption, Language, LanguageViews, Mint, Redeemers, RedeemersKey, RequiredSigners,
ScriptRef, TransactionBody, TransactionOutput, Tx, VKeyWitness, Value, WitnessSet,
},
AddrKeyhash, Hash, PlutusData, PlutusScript, PolicyId, PositiveCoin, TransactionInput,
Expand Down Expand Up @@ -1511,17 +1511,17 @@ fn tx_languages(mtx: &Tx, utxos: &UTxOs) -> Vec<Language> {
}
}
}
if !v1_scripts && !v2_scripts && !v3_scripts {
vec![]
} else if v1_scripts && !v2_scripts && !v3_scripts {
vec![Language::PlutusV1]
} else if !v1_scripts && v2_scripts && !v3_scripts {
vec![Language::PlutusV2]
} else if !v1_scripts && !v2_scripts && v3_scripts {
vec![Language::PlutusV3]
} else {
vec![Language::PlutusV1, Language::PlutusV2]
let mut langs = vec![];
if v1_scripts {
langs.push(Language::PlutusV1);
}
if v2_scripts {
langs.push(Language::PlutusV2);
}
if v3_scripts {
langs.push(Language::PlutusV3);
}
langs
}

// The metadata of the transaction is valid.
Expand Down Expand Up @@ -1557,13 +1557,13 @@ fn check_script_data_hash(
}
};

let Some(language_view) = cost_model_for_tx(&tx_languages, prot_pps) else {
let Some(language_views) = cost_model_for_tx(&tx_languages, prot_pps) else {
return Err(PostAlonzo(ScriptIntegrityHash));
};

let expected = pallas_primitives::conway::ScriptData::build_for(
&mtx.transaction_witness_set,
&Some(language_view),
&Some(language_views),
)
.ok_or(PostAlonzo(ScriptIntegrityHash))?
.hash();
Expand All @@ -1578,14 +1578,16 @@ fn check_script_data_hash(
fn cost_model_for_tx(
tx_languages: &[Language],
prot_pps: &ConwayProtParams,
) -> Option<LanguageView> {
let lang = itertools::max(tx_languages.iter())?;

let costs = match lang {
Language::PlutusV1 => prot_pps.cost_models_for_script_languages.plutus_v1.clone(),
Language::PlutusV2 => prot_pps.cost_models_for_script_languages.plutus_v2.clone(),
Language::PlutusV3 => prot_pps.cost_models_for_script_languages.plutus_v3.clone(),
};

costs.map(|costs| LanguageView(lang.clone() as u8, costs))
) -> Option<LanguageViews> {
let cost_models = &prot_pps.cost_models_for_script_languages;
let mut map = std::collections::BTreeMap::new();
for lang in tx_languages {
let costs = match lang {
Language::PlutusV1 => cost_models.plutus_v1.clone()?,
Language::PlutusV2 => cost_models.plutus_v2.clone()?,
Language::PlutusV3 => cost_models.plutus_v3.clone()?,
};
map.insert(lang.clone() as u8, costs);
}
Some(LanguageViews(map))
}
1 change: 1 addition & 0 deletions test_data/conway9.tx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
84a800d90102818258205173c41edccb5d79b811f965058ce867e80565a7e361d9332e51ebb841fa1dd1000181825839004693c0ac525d045cb0a4e75bd3adbd6956b3b744e88d21e041fc9b630df092006419e469e0c77876a499124bf903735b434c7989f7a8090a821b000000025400ada8a3581c186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4a146534f4449544102581c39c520d0627aafa728f7e4dd10142b77c257813c36f57e2cb88f72a5a146534f4449544102581c67f33146617a5e61936081db3b2117cbf59bd2123748f58ac9678656a146534f4449544102021a0002ef7909a3581c186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4a146534f4449544101581c39c520d0627aafa728f7e4dd10142b77c257813c36f57e2cb88f72a5a146534f4449544101581c67f33146617a5e61936081db3b2117cbf59bd2123748f58ac9678656a146534f44495441010b5820fcaa431d492db613dd7568732889ba12952507a1ea7abab1838c5c4a869ea22f0dd90102818258205173c41edccb5d79b811f965058ce867e80565a7e361d9332e51ebb841fa1dd10010825839004693c0ac525d045cb0a4e75bd3adbd6956b3b744e88d21e041fc9b630df092006419e469e0c77876a499124bf903735b434c7989f7a8090a821b0000000253ff35eba3581c186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4a146534f4449544101581c39c520d0627aafa728f7e4dd10142b77c257813c36f57e2cb88f72a5a146534f4449544101581c67f33146617a5e61936081db3b2117cbf59bd2123748f58ac9678656a146534f4449544101111a00046736a500d9010281825820e6e75c7876c610afd28a19c2947e72c1a629ab7903283e14915badee7c55402e5840b2717517a1df898c0a78d23714e719f8658e1e861ee142a26055989af58a5b30a3977b937ad48c225573ac8629c02f489da24076b1bb3c64e5bae806408bfa0903d90102814e4d0100003322222005120012001105a38201008200821901f419fa648201018200821904b01a0002afe48201028200821905781a00032ce406d901028152510100003222253330044a229309b2b2b9a107d901028146450101002499f5f6