partner_chains_cardano_offchain/
governance.rs

1use 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/// The supported Governance Policies are:
23/// - Plutus MultiSig implementation from partner-chain-smart-contracts
24/// - Native Script `atLeast` with only simple `sig` type of inner `scripts` field.
25#[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	/// Checks if the policy is 1 of 1 for given key hash
42	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	/// Checks if given key hash is among authorities
58	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/// Plutus MultiSig smart contract implemented in partner-chains-smart-contracts repo
71/// is legacy and ideally should have been used only with a single key in `governance init`.
72/// It allows minting the governance token only if the transaction in `required_singers` field
73/// has at least `threshold` number of key hashes from the `key_hashes` list.
74/// `threshold` and `key_hashes` are Plutus Data applied to the script.
75#[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/// This represents Cardano Native Script of type `atLeast`, where each of `scripts` has to be
83/// of type `sig`. We call them `key_hashes` to match our Partner Chains Plutus MultiSig policy.
84/// `threshold` field of this struct is mapped to `required` field in the simple script.
85#[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
183/// Returns decoded Governance Authorities and threshold if the policy script is an applied MultiSig policy.
184/// Returns None in case decoding failed, perhaps when some other policy is used.
185/// This method does not check for the policy itself. If invoked for a different policy, most probably will return None, with some chance of returning trash data.
186fn 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)]
228/// Summary of the M of N MultiSig governance policy.
229pub struct GovernancePolicySummary {
230	/// List of all key hashes of governance members.
231	pub key_hashes: Vec<ByteString>,
232	/// Minimum amount of governance signatures needed for governance action.
233	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
258/// Returns [GovernancePolicySummary] for partner chain identified by `genesis_utxo`.
259pub 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)]
272/// Parameters for multisig governance policy.
273pub struct MultiSigParameters {
274	governance_authorities: Vec<MainchainKeyHash>,
275	threshold: u8,
276}
277
278impl MultiSigParameters {
279	/// Constructs [MultiSigParameters] from governance authority member [MainchainKeyHash]es, and `threshold`.
280	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	/// Constructs [MultiSigParameters] with a single governance authority member.
294	pub fn new_one_of_one(governance_authority: &MainchainKeyHash) -> Self {
295		Self { governance_authorities: vec![*governance_authority], threshold: 1 }
296	}
297
298	/// Returns [SimpleAtLeastN] for this [MultiSigParameters].
299	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}