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
58/// Plutus MultiSig smart contract implemented in partner-chains-smart-contracts repo
59/// is legacy and ideally should have been used only with a single key in `governance init`.
60/// It allows minting the governance token only if the transaction in `required_singers` field
61/// has at least `threshold` number of key hashes from the `key_hashes` list.
62/// `threshold` and `key_hashes` are Plutus Data applied to the script.
63#[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/// This represents Cardano Native Script of type `atLeast`, where each of `scripts` has to be
71/// of type `sig`. We call them `key_hashes` to match our Partner Chains Plutus MultiSig policy.
72/// `threshold` field of this struct is mapped to `required` field in the simple script.
73#[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
171/// Returns decoded Governance Authorities and threshold if the policy script is an applied MultiSig policy.
172/// Returns None in case decoding failed, perhaps when some other policy is used.
173/// 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.
174fn 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)]
216/// Summary of the M of N MultiSig governance policy.
217pub struct GovernancePolicySummary {
218	/// List of all key hashes of governance members.
219	pub key_hashes: Vec<ByteString>,
220	/// Minimum amount of governance signatures needed for governance action.
221	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
246/// Returns [GovernancePolicySummary] for partner chain identified by `genesis_utxo`.
247pub 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)]
260/// Parameters for multisig governance policy.
261pub struct MultiSigParameters {
262	governance_authorities: Vec<MainchainKeyHash>,
263	threshold: u8,
264}
265
266impl MultiSigParameters {
267	/// Constructs [MultiSigParameters] from governance authority member [MainchainKeyHash]es, and `threshold`.
268	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	/// Constructs [MultiSigParameters] with a single governance authority member.
282	pub fn new_one_of_one(governance_authority: &MainchainKeyHash) -> Self {
283		Self { governance_authorities: vec![*governance_authority], threshold: 1 }
284	}
285
286	/// Returns [SimpleAtLeastN] for this [MultiSigParameters].
287	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}