feat(txbuilder): compute ScriptDataHash including edge cases (#525)

Co-authored-by: kalomaaan <kalomaaan@gmail.com>
This commit is contained in:
Santiago Carmuega 2024-10-22 22:20:58 -03:00 committed by GitHub
parent 061a7796d6
commit 698d7a4933
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 282 additions and 94 deletions

View file

@ -232,6 +232,14 @@ where
pub fn to_vec(self) -> Vec<(K, V)> { pub fn to_vec(self) -> Vec<(K, V)> {
self.into() self.into()
} }
pub fn from_vec(x: Vec<(K, V)>) -> Option<Self> {
if x.is_empty() {
None
} else {
Some(NonEmptyKeyValuePairs::Def(x))
}
}
} }
impl<K, V> From<NonEmptyKeyValuePairs<K, V>> for Vec<(K, V)> impl<K, V> From<NonEmptyKeyValuePairs<K, V>> for Vec<(K, V)>
@ -777,6 +785,14 @@ impl<T> NonEmptySet<T> {
pub fn to_vec(self) -> Vec<T> { pub fn to_vec(self) -> Vec<T> {
self.0 self.0
} }
pub fn from_vec(x: Vec<T>) -> Option<Self> {
if x.is_empty() {
None
} else {
Some(Self(x))
}
}
} }
impl<T> Deref for NonEmptySet<T> { impl<T> Deref for NonEmptySet<T> {

View file

@ -1,25 +1,26 @@
use std::ops::Deref; use std::ops::Deref;
use pallas_codec::utils::{CborWrap, KeyValuePairs}; use pallas_codec::utils::CborWrap;
use pallas_crypto::hash::Hash; use pallas_crypto::hash::Hash;
use pallas_primitives::{ use pallas_primitives::{
babbage::{ conway::{
DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, PlutusData, PlutusScript, DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, PlutusData,
PostAlonzoTransactionOutput, PseudoScript as PallasScript, PseudoTransactionOutput, PlutusScript, PostAlonzoTransactionOutput, PseudoScript as PallasScript,
Redeemer, RedeemerTag, TransactionBody, TransactionInput, Tx as BabbageTx, Value, PseudoTransactionOutput, Redeemer, RedeemerTag, TransactionBody, TransactionInput,
WitnessSet, Tx as BabbageTx, Value, WitnessSet,
}, },
Fragment, Fragment, NonEmptyKeyValuePairs, NonEmptySet, PositiveCoin,
}; };
use pallas_traverse::ComputeHash; use pallas_traverse::ComputeHash;
use crate::{ use crate::{
scriptdata,
transaction::{ transaction::{
model::{ model::{
BuilderEra, BuiltTransaction, DatumKind, ExUnits, Output, RedeemerPurpose, ScriptKind, BuilderEra, BuiltTransaction, DatumKind, ExUnits, Output, RedeemerPurpose, ScriptKind,
StagingTransaction, StagingTransaction,
}, },
opt_if_empty, Bytes, Bytes32, TransactionStatus, Bytes, Bytes32, TransactionStatus,
}, },
TxBuilderError, TxBuilderError,
}; };
@ -52,40 +53,43 @@ impl BuildBabbage for StagingTransaction {
.map(Output::build_babbage_raw) .map(Output::build_babbage_raw)
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let mint: Option<KeyValuePairs<Hash<28>, KeyValuePairs<_, _>>> = self.mint.map(|massets| { let mint = NonEmptyKeyValuePairs::from_vec(
massets self.mint
.deref()
.iter() .iter()
.flat_map(|x| x.deref().iter())
.map(|(pid, assets)| { .map(|(pid, assets)| {
( (
pid.0.into(), Hash::<28>::from(pid.0),
assets NonEmptyKeyValuePairs::from_vec(
.iter() assets
.map(|(n, x)| (n.clone().into(), *x)) .iter()
.collect::<Vec<_>>() .map(|(n, x)| (n.clone().into(), NonZeroInt::try_from(*x).unwrap()))
.into(), .collect::<Vec<_>>(),
)
.unwrap(),
) )
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>(),
.into() );
});
let collateral = self let collateral = NonEmptySet::from_vec(
.collateral_inputs self.collateral_inputs
.unwrap_or_default() .unwrap_or_default()
.iter() .iter()
.map(|x| TransactionInput { .map(|x| TransactionInput {
transaction_id: x.tx_hash.0.into(), transaction_id: x.tx_hash.0.into(),
index: x.txo_index, index: x.txo_index,
}) })
.collect(); .collect(),
);
let required_signers = self let required_signers = NonEmptySet::from_vec(
.disclosed_signers self.disclosed_signers
.unwrap_or_default() .unwrap_or_default()
.iter() .iter()
.map(|x| x.0.into()) .map(|x| x.0.into())
.collect(); .collect(),
);
let network_id = if let Some(nid) = self.network_id { let network_id = if let Some(nid) = self.network_id {
match NetworkId::try_from(nid) { match NetworkId::try_from(nid) {
@ -102,18 +106,19 @@ impl BuildBabbage for StagingTransaction {
.map(Output::build_babbage_raw) .map(Output::build_babbage_raw)
.transpose()?; .transpose()?;
let reference_inputs = self let reference_inputs = NonEmptySet::from_vec(
.reference_inputs self.reference_inputs
.unwrap_or_default() .unwrap_or_default()
.iter() .iter()
.map(|x| TransactionInput { .map(|x| TransactionInput {
transaction_id: x.tx_hash.0.into(), transaction_id: x.tx_hash.0.into(),
index: x.txo_index, index: x.txo_index,
}) })
.collect(); .collect(),
);
let (mut native_script, mut plutus_v1_script, mut plutus_v2_script) = let (mut native_script, mut plutus_v1_script, mut plutus_v2_script, mut plutus_v3_script) =
(vec![], vec![], vec![]); (vec![], vec![], vec![], vec![]);
for (_, script) in self.scripts.unwrap_or_default() { for (_, script) in self.scripts.unwrap_or_default() {
match script.kind { match script.kind {
@ -133,6 +138,11 @@ impl BuildBabbage for StagingTransaction {
plutus_v2_script.push(script) plutus_v2_script.push(script)
} }
ScriptKind::PlutusV3 => {
let script = PlutusScript::<3>(script.bytes.into());
plutus_v3_script.push(script)
}
} }
} }
@ -147,11 +157,11 @@ impl BuildBabbage for StagingTransaction {
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let mut mint_policies = mint let mut mint_policies = mint
.clone()
.unwrap_or(vec![].into())
.iter() .iter()
.map(|(p, _)| **p) .flat_map(|x| x.deref().iter())
.map(|(p, _)| *p)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
mint_policies.sort_unstable_by_key(|x| *x); mint_policies.sort_unstable_by_key(|x| *x);
let mut redeemers = vec![]; let mut redeemers = vec![];
@ -190,7 +200,7 @@ impl BuildBabbage for StagingTransaction {
RedeemerPurpose::Mint(pid) => { RedeemerPurpose::Mint(pid) => {
let index = mint_policies let index = mint_policies
.iter() .iter()
.position(|x| *x == pid.0) .position(|x| x.as_slice() == pid.0)
.ok_or(TxBuilderError::RedeemerTargetMissing)? .ok_or(TxBuilderError::RedeemerTargetMissing)?
as u32; as u32;
@ -205,34 +215,51 @@ impl BuildBabbage for StagingTransaction {
} }
}; };
let witness_set_redeemers = pallas_primitives::conway::Redeemers::List(
pallas_codec::utils::MaybeIndefArray::Def(redeemers.clone()),
);
let script_data_hash = self.language_view.map(|language_view| {
scriptdata::ScriptData {
redeemers: witness_set_redeemers.clone(),
datums: Some(plutus_data.clone()),
language_view,
}
.hash()
});
let mut pallas_tx = BabbageTx { let mut pallas_tx = BabbageTx {
transaction_body: TransactionBody { transaction_body: TransactionBody {
inputs, inputs: pallas_primitives::Set::from(inputs),
outputs, outputs,
ttl: self.invalid_from_slot, ttl: self.invalid_from_slot,
validity_interval_start: self.valid_from_slot, validity_interval_start: self.valid_from_slot,
fee: self.fee.unwrap_or_default(), fee: self.fee.unwrap_or_default(),
certificates: None, // TODO certificates: None, // TODO
withdrawals: None, // TODO withdrawals: None, // TODO
update: None, // TODO
auxiliary_data_hash: None, // TODO (accept user input) auxiliary_data_hash: None, // TODO (accept user input)
mint, mint,
script_data_hash: self.script_data_hash.map(|x| x.0.into()), script_data_hash,
collateral: opt_if_empty(collateral), collateral,
required_signers: opt_if_empty(required_signers), required_signers,
network_id, network_id,
collateral_return, collateral_return,
total_collateral: None, // TODO reference_inputs,
reference_inputs: opt_if_empty(reference_inputs), total_collateral: None, // TODO
voting_procedures: None, // TODO
proposal_procedures: None, // TODO
treasury_value: None, // TODO
donation: None, // TODO
}, },
transaction_witness_set: WitnessSet { transaction_witness_set: WitnessSet {
vkeywitness: None, vkeywitness: None,
native_script: opt_if_empty(native_script), native_script: NonEmptySet::from_vec(native_script),
bootstrap_witness: None, bootstrap_witness: None,
plutus_v1_script: opt_if_empty(plutus_v1_script), plutus_v1_script: NonEmptySet::from_vec(plutus_v1_script),
plutus_v2_script: opt_if_empty(plutus_v2_script), plutus_v2_script: NonEmptySet::from_vec(plutus_v2_script),
plutus_data: opt_if_empty(plutus_data), plutus_v3_script: NonEmptySet::from_vec(plutus_v3_script),
redeemer: opt_if_empty(redeemers), plutus_data: NonEmptySet::from_vec(plutus_data),
redeemer: Some(witness_set_redeemers),
}, },
success: true, // TODO success: true, // TODO
auxiliary_data: None.into(), // TODO auxiliary_data: None.into(), // TODO
@ -264,26 +291,27 @@ impl Output {
pub fn build_babbage_raw( pub fn build_babbage_raw(
&self, &self,
) -> Result<PseudoTransactionOutput<PostAlonzoTransactionOutput>, TxBuilderError> { ) -> Result<PseudoTransactionOutput<PostAlonzoTransactionOutput>, TxBuilderError> {
let value = if let Some(ref assets) = self.assets { let assets = NonEmptyKeyValuePairs::from_vec(
let txb_assets = assets self.assets
.deref()
.iter() .iter()
.flat_map(|x| x.deref().iter())
.map(|(pid, assets)| { .map(|(pid, assets)| {
( (
pid.0.into(), pid.0.into(),
assets assets
.iter() .iter()
.map(|(n, x)| (n.clone().into(), *x)) .map(|(n, x)| (n.clone().into(), PositiveCoin::try_from(*x).unwrap()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.into(), .try_into()
.unwrap(),
) )
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>(),
.into(); );
Value::Multiasset(self.lovelace, txb_assets) let value = match assets {
} else { Some(assets) => Value::Multiasset(self.lovelace, assets),
Value::Coin(self.lovelace) None => Value::Coin(self.lovelace),
}; };
let datum_option = if let Some(ref d) = self.datum { let datum_option = if let Some(ref d) = self.datum {
@ -318,6 +346,9 @@ impl Output {
ScriptKind::PlutusV2 => PallasScript::PlutusV2Script(PlutusScript::<2>( ScriptKind::PlutusV2 => PallasScript::PlutusV2Script(PlutusScript::<2>(
s.bytes.as_ref().to_vec().into(), s.bytes.as_ref().to_vec().into(),
)), )),
ScriptKind::PlutusV3 => PallasScript::PlutusV3Script(PlutusScript::<3>(
s.bytes.as_ref().to_vec().into(),
)),
}; };
Some(CborWrap(script)) Some(CborWrap(script))

View file

@ -1,4 +1,5 @@
mod babbage; mod babbage;
mod scriptdata;
mod transaction; mod transaction;
pub use babbage::BuildBabbage; pub use babbage::BuildBabbage;

View file

@ -0,0 +1,143 @@
use pallas_codec::minicbor::{self, Encode};
use pallas_primitives::conway::{CostModel, PlutusData, Redeemers};
use serde::{Deserialize, Serialize};
pub type PlutusVersion = u8;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LanguageView(pub PlutusVersion, pub CostModel);
impl<C> Encode<C> for LanguageView {
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();
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(())
}
}
}
}
pub struct ScriptData {
pub redeemers: Redeemers,
pub datums: Option<Vec<PlutusData>>,
pub language_view: LanguageView,
}
impl ScriptData {
pub fn hash(&self) -> pallas_crypto::hash::Hash<32> {
let mut buf = vec![];
minicbor::encode(&self.redeemers, &mut buf).unwrap(); // infallible
if let Some(datums) = &self.datums {
minicbor::encode(datums, &mut buf).unwrap(); // infallible
}
minicbor::encode(&self.language_view, &mut buf).unwrap(); // infallible
pallas_crypto::hash::Hasher::<256>::hash(&buf)
}
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use pallas_primitives::conway;
use pallas_traverse::MultiEraTx;
use super::*;
const 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,
16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, 178, 0, 1, 22151, 32, 91189, 769,
4, 2, 85848, 228465, 122, 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, 228465, 122,
0, 1, 1, 90434, 519, 0, 1, 74433, 32, 85848, 228465, 122, 0, 1, 1, 85848, 228465, 122,
0, 1, 1, 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,
53384111, 14333, 10,
]
});
static COST_MODEL_PLUTUS_V2: 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, 228465, 122, 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, 228465, 122,
0, 1, 1, 90434, 519, 0, 1, 74433, 32, 85848, 228465, 122, 0, 1, 1, 85848, 228465, 122,
0, 1, 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,
]
});
const TEST_VECTORS: LazyLock<Vec<(Vec<u8>, LanguageView)>> = LazyLock::new(|| {
vec![
(
hex::decode(include_str!("../../test_data/conway1.tx")).unwrap(),
LanguageView(1, COST_MODEL_PLUTUS_V2.clone()),
),
(
hex::decode(include_str!("../../test_data/conway2.tx")).unwrap(),
LanguageView(0, COST_MODEL_PLUTUS_V1.clone()),
),
]
});
fn assert_script_data_hash_matches(bytes: &[u8], language_view: &LanguageView) {
let tx = MultiEraTx::decode(&bytes).unwrap();
let tx = tx.as_conway().unwrap();
let witness = conway::WitnessSet::from(tx.transaction_witness_set.clone().unwrap());
let script_data = ScriptData {
redeemers: witness.redeemer.unwrap(),
datums: witness.plutus_data.map(|x| x.iter().cloned().collect()),
language_view: language_view.clone(),
};
let obtained = script_data.hash();
let expected = tx.transaction_body.script_data_hash.unwrap();
assert_eq!(obtained, expected);
}
#[test]
fn test_script_data_hash() {
for (bytes, language_view) in TEST_VECTORS.iter() {
assert_script_data_hash_matches(bytes, language_view);
}
}
}

View file

@ -20,7 +20,7 @@ pub struct Bytes64(pub [u8; 64]);
type PublicKey = Bytes32; type PublicKey = Bytes32;
type Signature = Bytes64; type Signature = Bytes64;
#[derive(Clone, PartialEq, Eq, Hash, Debug)] #[derive(Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub struct Hash28(pub [u8; 28]); pub struct Hash28(pub [u8; 28]);
#[derive(Clone, PartialEq, Eq, Hash, Debug)] #[derive(Clone, PartialEq, Eq, Hash, Debug)]
@ -52,12 +52,3 @@ pub type PolicyId = ScriptHash;
pub type DatumHash = Bytes32; pub type DatumHash = Bytes32;
pub type DatumBytes = Bytes; pub type DatumBytes = Bytes;
pub type AssetName = Bytes; pub type AssetName = Bytes;
/// If a Vec is empty, returns None, or Some(Vec) if not empty
pub fn opt_if_empty<T>(v: Vec<T>) -> Option<Vec<T>> {
if v.is_empty() {
None
} else {
Some(v)
}
}

View file

@ -10,7 +10,7 @@ use std::{collections::HashMap, ops::Deref};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::TxBuilderError; use crate::{scriptdata, TxBuilderError};
use super::{ use super::{
AssetName, Bytes, Bytes32, Bytes64, DatumBytes, DatumHash, Hash28, PolicyId, PubKeyHash, AssetName, Bytes, Bytes32, Bytes64, DatumBytes, DatumHash, Hash28, PolicyId, PubKeyHash,
@ -39,6 +39,7 @@ pub struct StagingTransaction {
pub script_data_hash: Option<Bytes32>, pub script_data_hash: Option<Bytes32>,
pub signature_amount_override: Option<u8>, pub signature_amount_override: Option<u8>,
pub change_address: Option<Address>, pub change_address: Option<Address>,
pub language_view: Option<scriptdata::LanguageView>,
// pub certificates: TODO // pub certificates: TODO
// pub withdrawals: TODO // pub withdrawals: TODO
// pub updates: TODO // pub updates: TODO
@ -233,6 +234,7 @@ impl StagingTransaction {
ScriptKind::Native => Hasher::<224>::hash_tagged(bytes.as_ref(), 0), ScriptKind::Native => Hasher::<224>::hash_tagged(bytes.as_ref(), 0),
ScriptKind::PlutusV1 => Hasher::<224>::hash_tagged(bytes.as_ref(), 1), ScriptKind::PlutusV1 => Hasher::<224>::hash_tagged(bytes.as_ref(), 1),
ScriptKind::PlutusV2 => Hasher::<224>::hash_tagged(bytes.as_ref(), 2), ScriptKind::PlutusV2 => Hasher::<224>::hash_tagged(bytes.as_ref(), 2),
ScriptKind::PlutusV3 => Hasher::<224>::hash_tagged(bytes.as_ref(), 3),
}; };
scripts.insert( scripts.insert(
@ -284,6 +286,17 @@ impl StagingTransaction {
self self
} }
pub fn language_view(mut self, plutus_version: ScriptKind, cost_model: Vec<i64>) -> Self {
self.language_view = match plutus_version {
ScriptKind::PlutusV1 => Some(scriptdata::LanguageView(0, cost_model)),
ScriptKind::PlutusV2 => Some(scriptdata::LanguageView(1, cost_model)),
ScriptKind::PlutusV3 => Some(scriptdata::LanguageView(2, cost_model)),
ScriptKind::Native => None,
};
self
}
pub fn add_spend_redeemer( pub fn add_spend_redeemer(
mut self, mut self,
input: Input, input: Input,
@ -340,17 +353,6 @@ impl StagingTransaction {
self self
} }
// TODO: script_data_hash computation
pub fn script_data_hash(mut self, hash: Hash<32>) -> Self {
self.script_data_hash = Some(Bytes32(*hash));
self
}
pub fn clear_script_data_hash(mut self) -> Self {
self.script_data_hash = None;
self
}
pub fn signature_amount_override(mut self, amount: u8) -> Self { pub fn signature_amount_override(mut self, amount: u8) -> Self {
self.signature_amount_override = Some(amount); self.signature_amount_override = Some(amount);
self self
@ -514,6 +516,7 @@ pub enum ScriptKind {
Native, Native,
PlutusV1, PlutusV1,
PlutusV2, PlutusV2,
PlutusV3,
} }
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]

View file

@ -479,6 +479,7 @@ mod tests {
signature_amount_override: Some(5), signature_amount_override: Some(5),
change_address: Some(Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap())), change_address: Some(Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap())),
script_data_hash: Some(Bytes32([0; 32])), script_data_hash: Some(Bytes32([0; 32])),
language_view: Some(crate::scriptdata::LanguageView(1, vec![1, 2, 3])),
}; };
let serialised_tx = serde_json::to_string(&tx).unwrap(); let serialised_tx = serde_json::to_string(&tx).unwrap();

1
test_data/conway1.tx Normal file
View file

@ -0,0 +1 @@
84a8008282582014f21123920de0ab51306f060daf332b2bc3daeba0a5933616cfd0a6fa05d57f00825820455363dd5e1a5b321908bb7ff6840c4a6c35d1d6b83eec5b2164ec741f5f7bac000183a200583901490c6c94eb76477d9316b80a2be491a830fdc56059539d318b32ea463c4b5f3adc80ca7729e1d4d67efab9244f254d32f446ef1db74037b301821a001a3a0ba1581cb1c62afb4c4e4af8881af2aec30205786b495b608157f254c0906670a14a465544676520436f696e1a02af707aa300583931905ab869961b094f1b8197278cfe15b45cbe49fa8f32c6b014f85a2db2f6abf60ccde92eae1a2f4fdf65f2eaf6208d872c6f0e597cc10b0701821a3b324430a2581c63f947b8d9535bc4e4ce6919e3dc056547e8d30ada12f29aa5f826b8a158207d1ee25d64f4cc5b1953b8fb307a7be9301d26646bf4092c122a231634623b5b01581cb1c62afb4c4e4af8881af2aec30205786b495b608157f254c0906670a14a465544676520436f696e1a2a37e777028201d81858e8d87989d87982581c63f947b8d9535bc4e4ce6919e3dc056547e8d30ada12f29aa5f826b858207d1ee25d64f4cc5b1953b8fb307a7be9301d26646bf4092c122a231634623b5bd879824040d87982581cb1c62afb4c4e4af8881af2aec30205786b495b608157f254c09066704a465544676520436f696e1b0000001166b125ce1a001373c2581cedbf33f5d6e083970648e39175c49ec1c093df76b6e6a0f1473e47761b000000028f200558581c8807fbe6e36b1c35ad6f36f0993e2fc67ab6f2db06041cfa3a53c04a581c554daf3e27e50c8c779713be6ba70f95c35f23f0552a28e5a7a340148258390122aea2da15e494e01767145d48bda16b6d437f1c449823a044193daf299a82ef56311aa10adf04c0072d4870eb9f4d5ff315132434841b741a00325aa0021a0005d11505a1581df196f5c1bee23481335ff4aece32fe1dfa1aa40a944a66d2d6edc9a9a5000b58203a98f88216e5025db28f06e90c56b3def9f35e3ac2beef37aef7f09ace7a60c00d81825820dd325cdb8c4b2d7fbc66d6e04f2abce41e52fbf55807948aed6b9abb726eb699000e81581cedbf33f5d6e083970648e39175c49ec1c093df76b6e6a0f1473e47761283825820c4a540ac2e06c217dd4fb3f39ca3863da394ba134677dafa9b98830ca71d584d03825820b91eda29d145ab6c0bc0d6b7093cb24b131440b7b015033205476f39c690a51f00825820b91eda29d145ab6c0bc0d6b7093cb24b131440b7b015033205476f39c690a51f01a2008182582014585da9857b30cf57edccac19d3012b602bb5650c7726a5f1504d9614d205225840563d66631ddd21c04ef19e2644bfaa28dd472c5fb6d72d6b290935f81b37b14eabe87d677b32bc73cdc244082d75a8d7fb8f73930b43f906cb9addd5a8fda2070583840000d87a80821a000186a01a01c9c380840001d879830101d87980821a000864701a0d1cef0084030080821a000668a01a09896800f5f6

1
test_data/conway2.tx Normal file

File diff suppressed because one or more lines are too long