partner_chains_cardano_offchain/
governance.rs1use crate::csl::{NetworkTypeExt, OgmiosUtxoExt};
2use crate::plutus_script;
3use crate::scripts_data;
4use cardano_serialization_lib::*;
5use ogmios_client::types::NativeScript as OgmiosNativeScript;
6use ogmios_client::{
7 query_ledger_state::QueryLedgerState, query_network::QueryNetwork, types::OgmiosUtxo,
8};
9use partner_chains_plutus_data::PlutusDataExtensions as _;
10use partner_chains_plutus_data::version_oracle::VersionOracleDatum;
11use serde::Serialize;
12use sidechain_domain::byte_string::ByteString;
13use sidechain_domain::{MainchainKeyHash, UtxoId};
14use std::fmt::Display;
15
16#[derive(Clone, Debug)]
17pub(crate) struct GovernanceData {
18 pub(crate) policy: GovernancePolicyScript,
19 pub(crate) utxo: OgmiosUtxo,
20}
21
22#[derive(Clone, Debug, Eq, PartialEq)]
26pub(crate) enum GovernancePolicyScript {
27 MultiSig(PartnerChainsMultisigPolicy),
28 AtLeastNNativeScript(SimpleAtLeastN),
29}
30
31impl GovernancePolicyScript {
32 pub(crate) fn script(&self) -> crate::csl::Script {
33 match self {
34 Self::MultiSig(policy) => crate::csl::Script::Plutus(policy.script.clone()),
35 Self::AtLeastNNativeScript(policy) => {
36 crate::csl::Script::Native(policy.to_csl_native_script())
37 },
38 }
39 }
40
41 pub(crate) fn is_single_key_policy_for(&self, key_hash: &Ed25519KeyHash) -> bool {
43 match self {
44 Self::MultiSig(PartnerChainsMultisigPolicy { script: _, key_hashes, threshold }) => {
45 *threshold == 1
46 && key_hashes.len() == 1
47 && key_hashes.iter().any(|h| &Ed25519KeyHash::from(*h) == key_hash)
48 },
49 Self::AtLeastNNativeScript(SimpleAtLeastN { threshold, key_hashes }) => {
50 *threshold == 1
51 && key_hashes.len() == 1
52 && key_hashes.iter().any(|h| &Ed25519KeyHash::from(*h) == key_hash)
53 },
54 }
55 }
56
57 pub(crate) fn contains_authority(&self, key_hash: &Ed25519KeyHash) -> bool {
59 match self {
60 Self::MultiSig(PartnerChainsMultisigPolicy { script: _, key_hashes, threshold: _ }) => {
61 key_hashes.iter().any(|h| &Ed25519KeyHash::from(*h) == key_hash)
62 },
63 Self::AtLeastNNativeScript(SimpleAtLeastN { threshold: _, key_hashes }) => {
64 key_hashes.iter().any(|h| &Ed25519KeyHash::from(*h) == key_hash)
65 },
66 }
67 }
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
76pub(crate) struct PartnerChainsMultisigPolicy {
77 pub(crate) script: plutus_script::PlutusScript,
78 pub(crate) key_hashes: Vec<[u8; 28]>,
79 pub(crate) threshold: u32,
80}
81
82#[derive(Clone, Debug, Eq, PartialEq)]
86pub(crate) struct SimpleAtLeastN {
87 pub(crate) threshold: u32,
88 pub(crate) key_hashes: Vec<[u8; 28]>,
89}
90
91impl SimpleAtLeastN {
92 pub fn to_csl_native_script(&self) -> NativeScript {
93 let mut native_scripts = NativeScripts::new();
94 for key_hash in self.key_hashes.clone() {
95 native_scripts.add(&NativeScript::new_script_pubkey(&ScriptPubkey::new(
96 &Ed25519KeyHash::from(key_hash),
97 )))
98 }
99 NativeScript::new_script_n_of_k(&ScriptNOfK::new(self.threshold, &native_scripts))
100 }
101}
102
103impl GovernanceData {
104 pub fn utxo_id(&self) -> sidechain_domain::UtxoId {
105 self.utxo.utxo_id()
106 }
107
108 pub(crate) fn utxo_id_as_tx_input(&self) -> TransactionInput {
109 TransactionInput::new(
110 &TransactionHash::from(self.utxo_id().tx_hash.0),
111 self.utxo_id().index.0.into(),
112 )
113 }
114
115 async fn get_governance_utxo<T: QueryLedgerState + QueryNetwork>(
116 genesis_utxo: UtxoId,
117 client: &T,
118 ) -> Result<Option<OgmiosUtxo>, JsError> {
119 let network = client
120 .shelley_genesis_configuration()
121 .await
122 .map_err(|e| {
123 JsError::from_str(&format!("Could not get Shelley Genesis Configuration: {}", e))
124 })?
125 .network;
126
127 let version_oracle_data = scripts_data::version_oracle(genesis_utxo, network.to_csl())
128 .map_err(|e| {
129 JsError::from_str(&format!(
130 "Could not get Version Oracle Script Data for: {}, {}",
131 genesis_utxo, e
132 ))
133 })?;
134
135 let utxos = client
136 .query_utxos(&[version_oracle_data.validator_address.clone()])
137 .await
138 .map_err(|e| {
139 JsError::from_str(&format!(
140 "Could not query Governance Validator UTxOs at {}: {}",
141 version_oracle_data.validator_address, e
142 ))
143 })?;
144
145 Ok(utxos.into_iter().find(|utxo| {
146 let correct_datum = utxo
147 .get_plutus_data()
148 .and_then(|plutus_data| VersionOracleDatum::try_from(plutus_data).ok())
149 .map(|data| data.version_oracle == 32)
150 .unwrap_or(false);
151
152 let contains_version_oracle_token =
153 utxo.value.native_tokens.contains_key(&version_oracle_data.policy_id().0);
154 correct_datum && contains_version_oracle_token
155 }))
156 }
157
158 pub(crate) async fn get<T: QueryLedgerState + QueryNetwork>(
159 genesis_utxo: UtxoId,
160 client: &T,
161 ) -> Result<GovernanceData, JsError> {
162 let utxo = Self::get_governance_utxo(genesis_utxo, client).await?.ok_or_else(|| JsError::from_str("Could not find governance versioning UTXO. This most likely means that governance was not properly set up on Cardano using governance init command."))?;
163 let policy = read_policy(&utxo)?;
164 Ok(GovernanceData { policy, utxo })
165 }
166}
167
168fn read_policy(governance_utxo: &OgmiosUtxo) -> Result<GovernancePolicyScript, JsError> {
169 let script = governance_utxo
170 .script
171 .clone()
172 .ok_or_else(|| JsError::from_str("No 'script' in governance UTXO"))?;
173 let plutus_multisig = script.clone().try_into().ok().and_then(parse_pc_multisig);
174 let policy_script = plutus_multisig.or_else(|| parse_simple_at_least_n_native_script(script));
175 policy_script.ok_or_else(|| {
176 JsError::from_str(&format!(
177 "Cannot convert script from UTXO {} into a multisig policy",
178 governance_utxo.utxo_id(),
179 ))
180 })
181}
182
183fn parse_pc_multisig(script: plutus_script::PlutusScript) -> Option<GovernancePolicyScript> {
187 script.unapply_data_csl().ok().and_then(|data| data.as_list()).and_then(|list| {
188 let mut it = list.into_iter();
189 let key_hashes = it.next().and_then(|data| {
190 data.as_list().map(|list| {
191 list.into_iter()
192 .filter_map(|item| item.as_bytes().and_then(|bytes| bytes.try_into().ok()))
193 .collect::<Vec<_>>()
194 })
195 })?;
196 let threshold: u32 = it.next().and_then(|t| t.as_u32())?;
197 Some(GovernancePolicyScript::MultiSig(PartnerChainsMultisigPolicy {
198 script,
199 key_hashes,
200 threshold,
201 }))
202 })
203}
204
205fn parse_simple_at_least_n_native_script(
206 script: ogmios_client::types::OgmiosScript,
207) -> Option<GovernancePolicyScript> {
208 match script.json {
209 Some(OgmiosNativeScript::Some { from, at_least }) => {
210 let mut keys = Vec::with_capacity(from.len());
211 for x in from {
212 let key = match x {
213 OgmiosNativeScript::Signature { from: key_hash } => Some(key_hash),
214 _ => None,
215 }?;
216 keys.push(key);
217 }
218 Some(GovernancePolicyScript::AtLeastNNativeScript(SimpleAtLeastN {
219 threshold: at_least,
220 key_hashes: keys,
221 }))
222 },
223 _ => None,
224 }
225}
226
227#[derive(Serialize)]
228pub struct GovernancePolicySummary {
230 pub key_hashes: Vec<ByteString>,
232 pub threshold: u32,
234}
235
236impl From<GovernancePolicyScript> for GovernancePolicySummary {
237 fn from(value: GovernancePolicyScript) -> Self {
238 match value {
239 GovernancePolicyScript::MultiSig(PartnerChainsMultisigPolicy {
240 script: _,
241 key_hashes,
242 threshold,
243 }) => GovernancePolicySummary {
244 threshold,
245 key_hashes: key_hashes.iter().map(|h| ByteString::from(h.to_vec())).collect(),
246 },
247 GovernancePolicyScript::AtLeastNNativeScript(SimpleAtLeastN {
248 threshold,
249 key_hashes,
250 }) => GovernancePolicySummary {
251 threshold,
252 key_hashes: key_hashes.iter().map(|h| ByteString::from(h.to_vec())).collect(),
253 },
254 }
255 }
256}
257
258pub async fn get_governance_policy_summary<T: QueryLedgerState + QueryNetwork>(
260 genesis_utxo: UtxoId,
261 client: &T,
262) -> Result<Option<GovernancePolicySummary>, JsError> {
263 if let Some(utxo) = GovernanceData::get_governance_utxo(genesis_utxo, client).await? {
264 let summary = read_policy(&utxo)?.into();
265 Ok(Some(summary))
266 } else {
267 Ok(None)
268 }
269}
270
271#[derive(Clone, PartialEq, Eq, Hash)]
272pub struct MultiSigParameters {
274 governance_authorities: Vec<MainchainKeyHash>,
275 threshold: u8,
276}
277
278impl MultiSigParameters {
279 pub fn new(governance_authorities: &[MainchainKeyHash], threshold: u8) -> Result<Self, &str> {
281 if governance_authorities.is_empty() {
282 return Err("governance authorities cannot be be empty");
283 }
284 if threshold == 0 {
285 return Err("threshold has to be a positive number");
286 }
287 if usize::from(threshold) > governance_authorities.len() {
288 return Err("threshold cannot be greater than governance authorities length");
289 }
290 Ok(Self { governance_authorities: governance_authorities.to_vec(), threshold })
291 }
292
293 pub fn new_one_of_one(governance_authority: &MainchainKeyHash) -> Self {
295 Self { governance_authorities: vec![*governance_authority], threshold: 1 }
296 }
297
298 pub(crate) fn as_simple_at_least_n(&self) -> SimpleAtLeastN {
300 SimpleAtLeastN {
301 threshold: self.threshold.into(),
302 key_hashes: self.governance_authorities.iter().map(|key_hash| key_hash.0).collect(),
303 }
304 }
305}
306
307impl Display for MultiSigParameters {
308 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309 f.write_str("Governance authorities:")?;
310 for authority in self.governance_authorities.iter() {
311 f.write_str(&format!("\n\t{}", &authority.to_hex_string()))?;
312 }
313 f.write_str(&format!("\nThreshold: {}", self.threshold))
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::{GovernancePolicySummary, read_policy};
320 use crate::{
321 governance::{GovernancePolicyScript, PartnerChainsMultisigPolicy, SimpleAtLeastN},
322 plutus_script::PlutusScript,
323 };
324 use hex_literal::hex;
325 use ogmios_client::types::OgmiosUtxo;
326
327 #[test]
328 fn read_pc_multisig_from_ogmios_utxo() {
329 let utxo_json = serde_json::json!({
330 "transaction": { "id": "57342ce4f30afa749bd78f0c093609366d997a1c4747d206ec7fd0aea9a35b55" },
331 "index": 0,
332 "address": "addr_test1wplvesjjxtg8lhyy34ak2dr9l3kz8ged3hajvcvpanfx7rcwzvtc5",
333 "value": { "ada": { "lovelace": 1430920 } },
334 "script": {
335 "language": "plutus:v2",
336 "cbor": "59020f0100003323322323232323322323232222323232532323355333573466e20cc8c8c88c008004c058894cd4004400c884cc018008c010004c04488004c04088008c01000400840304034403c4c02d24010350543500300d37586ae84008dd69aba1357440026eb0014c040894cd400440448c884c8cd40514cd4c00cc04cc030dd6198009a9803998009a980380411000a40004400290080a400429000180300119112999ab9a33710002900009807a490350543600133003001002301522253350011300f49103505437002215333573466e1d20000041002133005337020089001000980991299a8008806910a999ab9a3371e00a6eb800840404c0100048c8cc8848cc00400c008d55ce80098031aab9e00137540026016446666aae7c00480348cd4030d5d080118019aba2002498c02888cccd55cf8009006119a8059aba100230033574400493119319ab9c00100512200212200130062233335573e0024010466a00e6eb8d5d080118019aba20020031200123300122337000040029000180191299a800880211099a802801180200089100109109119800802001919180080091198019801001000a615f9f9f581ce8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b581c01010101010101010101010101010101010101010101010101010101581c02020202020202020202020202020202020202020202020202020202ff02ff0001"
337 }
338 });
339 let ogmios_utxo: OgmiosUtxo = serde_json::from_value(utxo_json).unwrap();
340 let policy = read_policy(&ogmios_utxo).unwrap();
341 assert_eq!(
342 policy,
343 GovernancePolicyScript::MultiSig(PartnerChainsMultisigPolicy {
344 script: PlutusScript {
345 bytes: hex!("59020f0100003323322323232323322323232222323232532323355333573466e20cc8c8c88c008004c058894cd4004400c884cc018008c010004c04488004c04088008c01000400840304034403c4c02d24010350543500300d37586ae84008dd69aba1357440026eb0014c040894cd400440448c884c8cd40514cd4c00cc04cc030dd6198009a9803998009a980380411000a40004400290080a400429000180300119112999ab9a33710002900009807a490350543600133003001002301522253350011300f49103505437002215333573466e1d20000041002133005337020089001000980991299a8008806910a999ab9a3371e00a6eb800840404c0100048c8cc8848cc00400c008d55ce80098031aab9e00137540026016446666aae7c00480348cd4030d5d080118019aba2002498c02888cccd55cf8009006119a8059aba100230033574400493119319ab9c00100512200212200130062233335573e0024010466a00e6eb8d5d080118019aba20020031200123300122337000040029000180191299a800880211099a802801180200089100109109119800802001919180080091198019801001000a615f9f9f581ce8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b581c01010101010101010101010101010101010101010101010101010101581c02020202020202020202020202020202020202020202020202020202ff02ff0001").to_vec(),
346 language: cardano_serialization_lib::Language::new_plutus_v2()
347 },
348 key_hashes: vec![hex!("e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b"), [1u8; 28], [2u8; 28]],
349 threshold: 2
350 })
351 )
352 }
353
354 #[test]
355 fn read_simple_at_least_n_native_script_from_ogmios_utxo() {
356 let utxo_json = serde_json::json!({
357 "transaction": { "id": "57342ce4f30afa749bd78f0c093609366d997a1c4747d206ec7fd0aea9a35b55" },
358 "index": 0,
359 "address": "addr_test1wplvesjjxtg8lhyy34ak2dr9l3kz8ged3hajvcvpanfx7rcwzvtc5",
360 "value": { "ada": { "lovelace": 1430920 } },
361 "script": {
362 "language": "native",
363 "json": {
364 "clause": "some",
365 "atLeast": 2,
366 "from": [
367 {
368 "clause": "signature",
369 "from": "e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b"
370 },
371 {
372 "clause": "signature",
373 "from": "a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7"
374 },
375 {
376 "clause": "signature",
377 "from": "b1b2b3b4b5b6b7b1b2b3b4b5b6b7b1b2b3b4b5b6b7b1b2b3b4b5b6b7"
378 }
379 ]
380 },
381 "cbor": "830301818200581ce8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b"
382 }
383 });
384 let ogmios_utxo: OgmiosUtxo = serde_json::from_value(utxo_json).unwrap();
385 let policy = read_policy(&ogmios_utxo).unwrap();
386 assert_eq!(
387 policy,
388 GovernancePolicyScript::AtLeastNNativeScript(SimpleAtLeastN {
389 threshold: 2,
390 key_hashes: vec![
391 hex!("e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b"),
392 hex!("a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7"),
393 hex!("b1b2b3b4b5b6b7b1b2b3b4b5b6b7b1b2b3b4b5b6b7b1b2b3b4b5b6b7")
394 ]
395 })
396 )
397 }
398
399 #[test]
400 fn simple_at_least_n_policy_to_json() {
401 let summary: GovernancePolicySummary =
402 GovernancePolicyScript::AtLeastNNativeScript(SimpleAtLeastN {
403 threshold: 2,
404 key_hashes: vec![
405 hex!("e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b"),
406 hex!("a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7"),
407 hex!("b1b2b3b4b5b6b7b1b2b3b4b5b6b7b1b2b3b4b5b6b7b1b2b3b4b5b6b7"),
408 ],
409 })
410 .into();
411 assert_eq!(
412 serde_json::to_value(summary).unwrap(),
413 serde_json::json!({
414 "key_hashes": [
415 "0xe8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b",
416 "0xa1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7a1a2a3a4a5a6a7",
417 "0xb1b2b3b4b5b6b7b1b2b3b4b5b6b7b1b2b3b4b5b6b7b1b2b3b4b5b6b7"
418 ],
419 "threshold": 2
420 }),
421 )
422 }
423
424 #[test]
425 fn pcsc_multisig_to_json() {
426 let summary: GovernancePolicySummary =
427 GovernancePolicyScript::MultiSig(PartnerChainsMultisigPolicy {
428 script: PlutusScript {
429 bytes: vec![],
430 language: cardano_serialization_lib::Language::new_plutus_v2(),
431 },
432 key_hashes: vec![
433 hex!("e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b"),
434 [1u8; 28],
435 [2u8; 28],
436 ],
437 threshold: 2,
438 })
439 .into();
440 assert_eq!(
441 serde_json::to_value(summary).unwrap(),
442 serde_json::json!({
443 "key_hashes": [
444 "0xe8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b",
445 "0x01010101010101010101010101010101010101010101010101010101",
446 "0x02020202020202020202020202020202020202020202020202020202"
447 ],
448 "threshold": 2
449 }),
450 )
451 }
452}