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