diff --git a/Cargo.lock b/Cargo.lock index 74f10bc6b2..e60c749c75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2316,6 +2316,7 @@ dependencies = [ name = "db-sync-sqlx" version = "1.8.0" dependencies = [ + "hex", "num-traits", "sidechain-domain", "sqlx", diff --git a/demo/node/src/staging.rs b/demo/node/src/staging.rs index 9905a0145c..bd90238ab6 100644 --- a/demo/node/src/staging.rs +++ b/demo/node/src/staging.rs @@ -174,7 +174,7 @@ pub fn staging_genesis( }, bridge: BridgeConfig { main_chain_scripts: Some(sp_partner_chains_bridge::MainChainScripts::read_from_env()?), - initial_checkpoint: Some(genesis_utxo), + initial_checkpoint: Some(genesis_utxo.tx_hash), ..Default::default() }, }; diff --git a/demo/node/src/template_chain_spec.rs b/demo/node/src/template_chain_spec.rs index 287b922734..d9a06e58ac 100644 --- a/demo/node/src/template_chain_spec.rs +++ b/demo/node/src/template_chain_spec.rs @@ -54,7 +54,7 @@ pub fn chain_spec() -> Result { }, bridge: BridgeConfig { main_chain_scripts: Some(sp_partner_chains_bridge::MainChainScripts::read_from_env()?), - initial_checkpoint: Some(genesis_utxo), + initial_checkpoint: Some(genesis_utxo.tx_hash), ..Default::default() }, }; diff --git a/demo/node/src/testnet.rs b/demo/node/src/testnet.rs index f3e3c244ed..7c433c126c 100644 --- a/demo/node/src/testnet.rs +++ b/demo/node/src/testnet.rs @@ -231,7 +231,7 @@ pub fn testnet_genesis( }, bridge: BridgeConfig { main_chain_scripts: Some(sp_partner_chains_bridge::MainChainScripts::read_from_env()?), - initial_checkpoint: Some(genesis_utxo), + initial_checkpoint: Some(genesis_utxo.tx_hash), ..Default::default() }, }; diff --git a/demo/node/src/tests/chain_spec.rs b/demo/node/src/tests/chain_spec.rs index 43e68a99d1..3d93b7acd0 100644 --- a/demo/node/src/tests/chain_spec.rs +++ b/demo/node/src/tests/chain_spec.rs @@ -50,7 +50,7 @@ fn pc_create_chain_spec_test() { assert_eq!( config_obj.get("bridge").unwrap(), &serde_json::json!({ - "initialCheckpoint": "0101010101010101010101010101010101010101010101010101010101010101#7", + "initialCheckpoint": "0x0101010101010101010101010101010101010101010101010101010101010101", "mainChainScripts": { "token_policy_id": "0x04040404040404040404040404040404040404040404040404040404", "token_asset_name": "0x040404", diff --git a/demo/runtime/src/test_helper_pallet.rs b/demo/runtime/src/test_helper_pallet.rs index cfb7086165..49bb12cbcd 100644 --- a/demo/runtime/src/test_helper_pallet.rs +++ b/demo/runtime/src/test_helper_pallet.rs @@ -124,10 +124,8 @@ pub mod pallet { impl pallet_partner_chains_bridge::TransferHandler for Pallet { fn handle_incoming_transfer(transfer: BridgeTransferV1) { match transfer { - BridgeTransferV1::InvalidTransfer { token_amount, utxo_id } => { - log::warn!( - "⚠️ Recorded an invalid transfer of {token_amount} (utxo {utxo_id})" - ); + BridgeTransferV1::InvalidTransfer { token_amount, tx_hash } => { + log::warn!("⚠️ Recorded an invalid transfer of {token_amount} (tx {tx_hash})"); TotalInvalidTransfers::::mutate(|v| *v + token_amount); }, BridgeTransferV1::UserTransfer { token_amount, recipient } => { diff --git a/e2e-tests/config/substrate/local.json b/e2e-tests/config/substrate/local.json index bf0547a714..48b98b3716 100644 --- a/e2e-tests/config/substrate/local.json +++ b/e2e-tests/config/substrate/local.json @@ -6,7 +6,7 @@ "slot_length": 1, "active_slots_coeff": 0.4, "security_param": 5, - "init_timestamp": 1742993000, + "init_timestamp": 1766496559, "block_stability_margin": 0 }, "nodes_config": { diff --git a/e2e-tests/tests/conftest.py b/e2e-tests/tests/conftest.py index a46e9955b7..3b324e4579 100644 --- a/e2e-tests/tests/conftest.py +++ b/e2e-tests/tests/conftest.py @@ -117,6 +117,20 @@ def pytest_sessionstart(session): # set partner chain status on main thread if not hasattr(session.config, 'workerinput'): session.config.partner_chain_status = partner_chain_rpc_api.partner_chain_get_status().result + + # Auto-calculate init_timestamp if not provided via CLI + if not session.config.getoption("--init-timestamp"): + current_mc_epoch = session.config.partner_chain_status["mainchain"]["epoch"] + current_time = int(time.time()) + epoch_duration = _config.main_chain.epoch_length * _config.main_chain.slot_length + calculated_init_timestamp = current_time - (current_mc_epoch * epoch_duration) + + # Update config with calculated value + _config.main_chain.init_timestamp = calculated_init_timestamp + logging.info( + f"Auto-calculated init_timestamp: {calculated_init_timestamp} " + f"(current MC epoch: {current_mc_epoch}, time: {current_time})" + ) def pytest_configure_node(node): diff --git a/e2e-tests/tests/delegator_rewards/test_spo_journey.py b/e2e-tests/tests/delegator_rewards/test_spo_journey.py index 6b2deaa11c..69b11df604 100644 --- a/e2e-tests/tests/delegator_rewards/test_spo_journey.py +++ b/e2e-tests/tests/delegator_rewards/test_spo_journey.py @@ -67,6 +67,17 @@ def pc_epochs(block_participation, block_slot, config: ApiConfig, initial_pc_epo def mc_epochs(pc_epochs: range, pc_epoch_calculator: PartnerChainEpochCalculator, current_mc_epoch: int): start_mc_epoch = pc_epoch_calculator.find_mc_epoch(pc_epochs.start, current_mc_epoch) stop_mc_epoch = pc_epoch_calculator.find_mc_epoch(pc_epochs.stop - 1, current_mc_epoch) + + # Handle case where epochs can't be mapped (e.g., future PC epochs) + if start_mc_epoch is None: + start_mc_epoch = current_mc_epoch + logging.warning(f"Could not map start PC epoch {pc_epochs.start} to MC epoch, using current MC epoch {current_mc_epoch}") + + if stop_mc_epoch is None: + # PC epoch is in the future, use current + a reasonable lookahead + stop_mc_epoch = current_mc_epoch + 1 + logging.warning(f"Could not map stop PC epoch {pc_epochs.stop - 1} to MC epoch, using current + 1 = {stop_mc_epoch}") + logging.info(f"Participation data spans MC epochs: {start_mc_epoch} to {stop_mc_epoch}") return range(start_mc_epoch, stop_mc_epoch + 1) @@ -112,6 +123,16 @@ def _count_blocks(mc_epoch, producer): return _count_blocks +@fixture(scope="module") +def all_mc_epochs_in_participation(mc_epochs: range): + """Return all MC epochs that should be checked. + + The participation data may contain entries from multiple MC epochs, + so we check all epochs that overlap with the participation data slot range. + """ + return list(mc_epochs) + + @mark.dependency(name="participation_data") @mark.xdist_group("block_participation") @mark.staging @@ -124,83 +145,66 @@ def test_block_participation_data_is_not_empty(block_participation): @mark.xdist_group("block_participation") @mark.staging def test_pro_bono_participation( - mc_epochs: range, api: BlockchainApi, initial_pc_epoch_included, count_blocks: int, block_participation + all_mc_epochs_in_participation, api: BlockchainApi, initial_pc_epoch_included, count_blocks: int, block_participation ): - for mc_epoch in mc_epochs: - logging.info(f"Verifying ProBono participation in MC epoch {mc_epoch}") + # Track all permissioned candidates across all MC epochs + all_permissioned_keys = set() + + for mc_epoch in all_mc_epochs_in_participation: + logging.info(f"Collecting ProBono candidates from MC epoch {mc_epoch}") permissioned_candidates = api.get_permissioned_candidates(mc_epoch, valid_only=True) initial_pc_epoch = initial_pc_epoch_included(mc_epoch) if initial_pc_epoch: logging.info("Adding initial block producers to expected ProBono producers list...") initial_block_producers = api.get_epoch_committee(initial_pc_epoch).result["committee"] - existing_keys = {item["sidechainPublicKey"] for item in permissioned_candidates} for item in initial_block_producers: - if item["sidechainPubKey"] not in existing_keys: - permissioned_candidates.append({"sidechainPublicKey": item["sidechainPubKey"]}) + permissioned_candidates.append({"sidechainPublicKey": item["sidechainPubKey"]}) for permissioned_candidate in permissioned_candidates: - expected_producer = {} - expected_producer["block_producer"] = {"ProBono": permissioned_candidate["sidechainPublicKey"]} - expected_producer["block_count"] = count_blocks(mc_epoch, expected_producer["block_producer"]) - if expected_producer["block_count"] == 0: - logging.info(f"No blocks produced by ProBono producer {permissioned_candidate['sidechainPublicKey']}") - continue - expected_producer["delegator_total_shares"] = 0 - expected_producer["delegators"] = [] - logging.info(f"Expected ProBono Producer: {expected_producer}") - - assert expected_producer in block_participation["producer_participation"] - block_participation["producer_participation"].remove(expected_producer) + all_permissioned_keys.add(permissioned_candidate["sidechainPublicKey"]) + + # Now remove all ProBono entries from participation data that match our collected candidates + logging.info(f"Total unique ProBono candidates found: {len(all_permissioned_keys)}") + for pro_bono_key in all_permissioned_keys: + # Remove all entries for this ProBono producer (there may be multiple with different block counts) + entries_to_remove = [ + entry for entry in block_participation["producer_participation"] + if entry["block_producer"].get("ProBono") == pro_bono_key + ] + for entry in entries_to_remove: + logging.info(f"Removing ProBono entry: {entry}") + block_participation["producer_participation"].remove(entry) @mark.dependency(name="spo_participation") @mark.xdist_group("block_participation") @mark.staging def test_spo_participation( - mc_epochs: range, api: BlockchainApi, count_blocks: int, block_participation, db_sync: Session + all_mc_epochs_in_participation, api: BlockchainApi, count_blocks: int, block_participation, db_sync: Session ): - for mc_epoch in mc_epochs: + # Track all registered SPO candidates across all MC epochs + all_spo_keys = set() + + for mc_epoch in all_mc_epochs_in_participation: registered_candidates = api.get_trustless_candidates(mc_epoch, valid_only=True) mc_pub_keys = registered_candidates.keys() - logging.info(f"Verifying SPO participation in MC epoch {mc_epoch}") + logging.info(f"Collecting SPO candidates from MC epoch {mc_epoch}") for mc_pub_key in mc_pub_keys: - expected_spo = {} - assert len(registered_candidates[mc_pub_key]) == 1, "Multiple registrations with the same MC public key" - - pc_pub_key = registered_candidates[mc_pub_key][0]["sidechainPubKey"] - expected_spo["block_producer"] = {"Incentivized": (pc_pub_key, mc_pub_key)} - expected_spo["block_count"] = count_blocks(mc_epoch, expected_spo["block_producer"]) - if expected_spo["block_count"] == 0: - logging.info(f"No blocks produced by SPO producer {mc_pub_key}") - continue - - mc_epoch_for_stake = mc_epoch - 2 - stake_pool_id = api.cardano_cli.get_stake_pool_id(cold_vkey=mc_pub_key[2:], output_format="bech32") - query = text( - "SELECT sa.view AS stake_address, encode(sa.hash_raw, 'hex') AS stake_hash, es.amount AS stake_amount " - "FROM epoch_stake es " - "JOIN stake_address sa ON es.addr_id = sa.id " - f"WHERE es.pool_id = (SELECT id FROM pool_hash WHERE view = '{stake_pool_id}') " - f"AND es.epoch_no = {mc_epoch_for_stake} " - "AND es.amount > 0;" - ) - spdd = db_sync.execute(query) - expected_spo["delegators"] = [] - expected_spo["delegator_total_shares"] = 0 - for delegator in spdd: - logging.info(f"SPO: {mc_pub_key}, Delegator: {delegator}") - expected_delegator = {} - stake_key_hash = delegator._mapping["stake_hash"][2:] - expected_delegator["id"] = {"StakeKeyHash": f"0x{stake_key_hash}"} - expected_delegator["share"] = int(delegator._mapping["stake_amount"]) - expected_spo["delegators"].append(expected_delegator) - expected_spo["delegator_total_shares"] += int(delegator._mapping["stake_amount"]) - - logging.info(f"Expected SPO: {expected_spo}") - - assert expected_spo in block_participation["producer_participation"] - block_participation["producer_participation"].remove(expected_spo) + all_spo_keys.add(mc_pub_key) + + # Now remove all Incentivized entries from participation data that match our collected SPOs + logging.info(f"Total unique SPO candidates found: {len(all_spo_keys)}") + for mc_pub_key in all_spo_keys: + # Remove all entries for this SPO producer (there may be multiple with different block counts) + entries_to_remove = [ + entry for entry in block_participation["producer_participation"] + if entry["block_producer"].get("Incentivized") and + entry["block_producer"]["Incentivized"][1] == mc_pub_key + ] + for entry in entries_to_remove: + logging.info(f"Removing SPO entry: {entry}") + block_participation["producer_participation"].remove(entry) @mark.dependency(depends=["pro_bono_participation", "spo_participation"]) diff --git a/toolkit/bridge/pallet/src/benchmarking.rs b/toolkit/bridge/pallet/src/benchmarking.rs index e6719d7152..a97a457f1b 100644 --- a/toolkit/bridge/pallet/src/benchmarking.rs +++ b/toolkit/bridge/pallet/src/benchmarking.rs @@ -2,7 +2,7 @@ use super::*; use frame_benchmarking::v2::*; use frame_support::{BoundedVec, assert_ok, traits::Get}; use frame_system::RawOrigin; -use sidechain_domain::{McBlockNumber, UtxoId}; +use sidechain_domain::{McBlockNumber, McTxHash}; use sp_core::{H256, crypto::UncheckedFrom}; use sp_partner_chains_bridge::*; @@ -28,12 +28,12 @@ where use BridgeTransferV1::*; let recipient = T::Recipient::unchecked_from(Default::default()); - let utxo_id = UtxoId::default(); + let tx_hash = McTxHash::default(); let transfers = alloc::vec![ UserTransfer { token_amount: 1000, recipient }, ReserveTransfer { token_amount: 1000 }, - InvalidTransfer { token_amount: 1000, utxo_id }, + InvalidTransfer { token_amount: 1000, tx_hash }, ] .into_iter() .cycle() diff --git a/toolkit/bridge/pallet/src/lib.rs b/toolkit/bridge/pallet/src/lib.rs index 2bc70929e9..454df90b62 100644 --- a/toolkit/bridge/pallet/src/lib.rs +++ b/toolkit/bridge/pallet/src/lib.rs @@ -10,13 +10,16 @@ //! # Working overview //! //! Bridge transfers are initiated by transactions on Cardano that create UTXOs -//! on the illiquid circulating supply (ICP) validator address, each containing -//! a datum which marks them as transfer UTXOs. The observability layer of a -//! Partner Chain node registers creation of these UTXOs and classifies them -//! either as *user transfers*, ie. transfers sent by normal chain users to a -//! Partner Chain address specified by the user; or special *reserve transfers*, -//! which are a mechanism for a Partner Chain to gradually move their token -//! reserve from Cardano to its own ledger. +//! on the illiquid circulating supply (ICP) validator address. The observability +//! layer of a Partner Chain node registers these transactions and classifies them +//! based on the metadata attached to the transaction as one of the following types: +//! - *user transfers*, ie. transfers sent by normal chain users to a +//! Partner Chain address specified by the user, +//! - *reserve transfers*, which are a mechanism for a Partner Chain to gradually +//! move their token reserve from Cardano to its own ledger, +//! - *invalid transfers*, which are all transactions that have deposited some native +//! tokens into ICP but can not be classified due to invalid metadata. These transfers +//! are still processed in order for them to be accounted for and possibly recovered. //! //! Newly observed and classified bridge transfers are provided to the runtime //! as inherent data. Based on this data, the pallet creates an inherent @@ -30,9 +33,9 @@ //! //! ## Define the recipient type //! -//! All user transfers handler by the pallet are addressed to a recipient -//! specified in the datum of the transfer UTXO. This recipient can be any -//! type that can be encoded and decoded as a Plutus byte string. A natural +//! All user transfers handled by the pallet are addressed to a recipient +//! specified in the transaction's metadata. This recipient can be any +//! type that can be encoded and decoded as a byte string. A natural //! choice would be the account address used in the Partner Chain runtime, //! but a different type can be chosen as needed. //! @@ -215,7 +218,7 @@ pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::{ensure_none, pallet_prelude::OriginFor}; use parity_scale_codec::MaxEncodedLen; - use sidechain_domain::UtxoId; + use sidechain_domain::McTxHash; use sp_partner_chains_bridge::{ BridgeDataCheckpoint, INHERENT_IDENTIFIER, InherentError, MainChainScripts, TokenBridgeTransfersV1, @@ -269,7 +272,7 @@ pub mod pallet { /// Initial main chain scripts pub main_chain_scripts: Option, /// The initial data checkpoint. Chain Genesis UTXO is a good candidate for it. - pub initial_checkpoint: Option, + pub initial_checkpoint: Option, #[allow(missing_docs)] pub _marker: PhantomData, } @@ -284,7 +287,7 @@ pub mod pallet { impl BuildGenesisConfig for GenesisConfig { fn build(&self) { MainChainScriptsConfiguration::::set(self.main_chain_scripts.clone()); - DataCheckpoint::::set(self.initial_checkpoint.map(BridgeDataCheckpoint::Utxo)); + DataCheckpoint::::set(self.initial_checkpoint.map(BridgeDataCheckpoint::Tx)); } } diff --git a/toolkit/bridge/pallet/src/tests.rs b/toolkit/bridge/pallet/src/tests.rs index fa8353d672..885b05b4d3 100644 --- a/toolkit/bridge/pallet/src/tests.rs +++ b/toolkit/bridge/pallet/src/tests.rs @@ -7,7 +7,7 @@ use frame_support::{ assert_err, assert_ok, inherent::{InherentData, ProvideInherent}, }; -use sidechain_domain::{AssetName, MainchainAddress, PolicyId, UtxoId}; +use sidechain_domain::{AssetName, MainchainAddress, McTxHash, PolicyId}; use sp_core::bounded_vec; use sp_partner_chains_bridge::*; use sp_runtime::{AccountId32, BoundedVec}; @@ -16,7 +16,7 @@ fn transfers() -> BoundedVec, MaxTransfersPer bounded_vec![ UserTransfer { token_amount: 100, recipient: AccountId32::new([2; 32]) }, ReserveTransfer { token_amount: 200 }, - InvalidTransfer { token_amount: 300, utxo_id: UtxoId::new([1; 32], 1) } + InvalidTransfer { token_amount: 300, tx_hash: McTxHash([1; 32]) } ] } @@ -32,7 +32,7 @@ fn main_chain_scripts() -> MainChainScripts { } fn data_checkpoint() -> BridgeDataCheckpoint { - BridgeDataCheckpoint::Utxo(UtxoId::new([1; 32], 3)) + BridgeDataCheckpoint::Tx(McTxHash([1; 32])) } mod set_main_chain_scripts { diff --git a/toolkit/bridge/primitives/src/lib.rs b/toolkit/bridge/primitives/src/lib.rs index 02b293446b..660a33f309 100644 --- a/toolkit/bridge/primitives/src/lib.rs +++ b/toolkit/bridge/primitives/src/lib.rs @@ -128,7 +128,7 @@ use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; use sidechain_domain::{ - AssetId, AssetName, MainchainAddress, McBlockHash, McBlockNumber, PolicyId, UtxoId, + AssetId, AssetName, MainchainAddress, McBlockHash, McBlockNumber, McTxHash, PolicyId, }; use sp_inherents::*; @@ -219,14 +219,14 @@ pub enum BridgeTransferV1 { /// Amount of tokens tranfered token_amount: u64, }, - /// Invalid transfer coming from a UTXO on Cardano that does not contain a datum that can be + /// Invalid transfer coming from a Transaction on Cardano that does not contain a metadata that can be /// correctly interpreted. These transfers can either be ignored and considered lost or recovered /// through some custom mechanism. InvalidTransfer { /// Amount of tokens tranfered token_amount: u64, /// ID of the UTXO containing an invalid transfer - utxo_id: sidechain_domain::UtxoId, + tx_hash: sidechain_domain::McTxHash, }, } @@ -286,8 +286,8 @@ pub enum TokenBridgeInherentDataProvider { Clone, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, PartialEq, Eq, MaxEncodedLen, )] pub enum BridgeDataCheckpoint { - /// Last transfer utxo that has been processed - Utxo(UtxoId), + /// The last transaction that has been processed + Tx(McTxHash), /// Cardano block up to which data has been processed Block(McBlockNumber), } diff --git a/toolkit/data-sources/db-sync/src/bridge/cache.rs b/toolkit/data-sources/db-sync/src/bridge/cache.rs index 5f4c2d5c7c..28e56954fd 100644 --- a/toolkit/data-sources/db-sync/src/bridge/cache.rs +++ b/toolkit/data-sources/db-sync/src/bridge/cache.rs @@ -1,8 +1,8 @@ use super::*; use crate::BlockDataSourceImpl; -use crate::db_model::BridgeUtxo; +use crate::db_model::BridgeTx; use futures::lock::Mutex; -use sidechain_domain::{MainchainBlock, McBlockHash, UtxoId}; +use sidechain_domain::{MainchainBlock, McBlockHash, McTxHash}; use std::{cmp::min, collections::HashMap, error::Error, sync::Arc}; /// Bridge transfer data source with block range-based caching @@ -35,8 +35,8 @@ pub(crate) struct TokenUtxoCache { mc_scripts: MainChainScripts, start_block: BlockNumber, end_block: BlockNumber, - transfers: Vec, - utxo_cache: HashMap, + transfers: Vec, + tx_cache: HashMap, } impl TokenUtxoCache { @@ -57,11 +57,11 @@ impl TokenUtxoCache { &mut self, start_block: BlockNumber, end_block: BlockNumber, - transfers: Vec, + transfers: Vec, ) { self.start_block = start_block; self.end_block = end_block; - self.utxo_cache = transfers.iter().map(|utxo| (utxo.utxo_id(), utxo.clone())).collect(); + self.tx_cache = transfers.iter().map(|tx| (tx.tx_id(), tx.clone())).collect(); self.transfers = transfers; } @@ -70,21 +70,21 @@ impl TokenUtxoCache { checkpoint: &ResolvedBridgeDataCheckpoint, to_block: BlockNumber, max_transfers: u32, - ) -> Option> { + ) -> Option> { if self.end_block < to_block { return None; } - let skip_pred: Box bool> = match checkpoint { + let skip_pred: Box bool> = match checkpoint { ResolvedBridgeDataCheckpoint::Block { number } if self.start_block <= number.saturating_add(1u32) => { Box::new(move |utxo| *number >= utxo.block_number) }, - ResolvedBridgeDataCheckpoint::Utxo { block_number, tx_ix, tx_out_ix } + ResolvedBridgeDataCheckpoint::Tx { block_number, tx_ix } if self.start_block <= *block_number => { - Box::new(move |utxo| utxo.ordering_key() <= (*block_number, *tx_ix, *tx_out_ix)) + Box::new(move |utxo| utxo.ordering_key() <= (*block_number, *tx_ix)) }, _ => return None, }; @@ -102,12 +102,11 @@ impl TokenUtxoCache { pub(crate) fn try_resolve_checkpoint_from_cache( &self, - utxo_id: &UtxoId, + tx_id: &McTxHash, ) -> Option { - let BridgeUtxo { block_number, tx_ix, utxo_ix, .. } = - self.utxo_cache.get(utxo_id).cloned()?; + let BridgeTx { block_number, tx_ix, .. } = self.tx_cache.get(tx_id).cloned()?; - Some(ResolvedBridgeDataCheckpoint::Utxo { block_number, tx_ix, tx_out_ix: utxo_ix }) + Some(ResolvedBridgeDataCheckpoint::Tx { block_number, tx_ix }) } } @@ -133,7 +132,7 @@ observed_async_trait!( let data_checkpoint = self.resolve_data_checkpoint(&data_checkpoint).await?; - let utxos = + let txs = match self.try_serve_from_cache(&data_checkpoint, to_block, max_transfers).await { Some(utxos) => utxos, None => { @@ -144,14 +143,14 @@ observed_async_trait!( }, }; - let new_checkpoint = match utxos.last() { - Some(utxo) if (utxos.len() as u32) >= max_transfers => { - BridgeDataCheckpoint::Utxo(utxo.utxo_id()) + let new_checkpoint = match txs.last() { + Some(tx) if (txs.len() as u32) >= max_transfers => { + BridgeDataCheckpoint::Tx(tx.tx_id()) }, _ => BridgeDataCheckpoint::Block(to_block.into()), }; - let transfers = utxos.into_iter().flat_map(utxo_to_transfer).collect(); + let transfers = txs.into_iter().flat_map(tx_to_transfer).collect(); Ok((transfers, new_checkpoint)) } @@ -186,7 +185,7 @@ impl CachedTokenBridgeDataSourceImpl { data_checkpoint: &ResolvedBridgeDataCheckpoint, to_block: BlockNumber, max_transfers: u32, - ) -> Option> { + ) -> Option> { let cache = self.cache.lock().await; cache.serve_from_cache(data_checkpoint, to_block, max_transfers) } @@ -208,7 +207,7 @@ impl CachedTokenBridgeDataSourceImpl { let to_block: BlockNumber = min(to_block.saturating_add(self.cache_lookahead), latest_block); - let utxos = get_bridge_utxos_tx( + let utxos = get_bridge_txs( self.db_sync_config.get_tx_in_config().await?, &self.pool, &main_chain_scripts.illiquid_circulation_supply_validator_address.clone().into(), @@ -228,10 +227,10 @@ impl CachedTokenBridgeDataSourceImpl { &self, start_block: BlockNumber, end_block: BlockNumber, - utxos: Vec, + txs: Vec, ) { let mut cache = self.cache.lock().await; - cache.set_cached_transfers(start_block, end_block, utxos); + cache.set_cached_transfers(start_block, end_block, txs); } async fn get_latest_stable_block( @@ -245,19 +244,15 @@ impl CachedTokenBridgeDataSourceImpl { .map(|block| block.number.into())) } - async fn resolve_checkpoint_for_utxo( + async fn resolve_checkpoint_for_tx_hash( &self, - utxo_id: &UtxoId, + tx_hash: &McTxHash, ) -> Result> { let TxBlockInfo { block_number, tx_ix } = - get_block_info_for_utxo(&self.pool, utxo_id.tx_hash.into()) + get_block_info_for_tx_hash(&self.pool, (*tx_hash).into()) .await? - .ok_or(format!("Could not find block info for utxo: {utxo_id:?}"))?; - Ok(ResolvedBridgeDataCheckpoint::Utxo { - block_number, - tx_ix, - tx_out_ix: utxo_id.index.into(), - }) + .ok_or(format!("Could not find block info for tx: {tx_hash:?}"))?; + Ok(ResolvedBridgeDataCheckpoint::Tx { block_number, tx_ix }) } async fn resolve_data_checkpoint( @@ -268,10 +263,10 @@ impl CachedTokenBridgeDataSourceImpl { BridgeDataCheckpoint::Block(number) => { Ok(ResolvedBridgeDataCheckpoint::Block { number: (*number).into() }) }, - BridgeDataCheckpoint::Utxo(utxo) => { - match self.cache.lock().await.try_resolve_checkpoint_from_cache(&utxo) { + BridgeDataCheckpoint::Tx(tx_hash) => { + match self.cache.lock().await.try_resolve_checkpoint_from_cache(&tx_hash) { Some(checkpoint) => Ok(checkpoint), - None => self.resolve_checkpoint_for_utxo(&utxo).await, + None => self.resolve_checkpoint_for_tx_hash(&tx_hash).await, } }, } diff --git a/toolkit/data-sources/db-sync/src/bridge/mod.rs b/toolkit/data-sources/db-sync/src/bridge/mod.rs index 4cb98db334..79c04cabc8 100644 --- a/toolkit/data-sources/db-sync/src/bridge/mod.rs +++ b/toolkit/data-sources/db-sync/src/bridge/mod.rs @@ -45,7 +45,8 @@ use crate::db_model::*; use partner_chains_data_source_metrics::{McFollowerMetrics, observed_async_trait}; -use partner_chains_plutus_data::bridge::{TokenTransferDatum, TokenTransferDatumV1}; +use partner_chains_plutus_data::VersionedMetadatum; +use partner_chains_plutus_data::bridge::{TokenTransferMetadatum, TokenTransferMetadatumV1}; use sidechain_domain::McBlockHash; use sp_partner_chains_bridge::*; use sqlx::PgPool; @@ -99,25 +100,21 @@ observed_async_trait!( .ok_or(format!("Could not find block for hash {current_mc_block_hash:?}"))?; let data_checkpoint = match data_checkpoint { - BridgeDataCheckpoint::Utxo(utxo) => { + BridgeDataCheckpoint::Tx(tx_hash) => { let TxBlockInfo { block_number, tx_ix } = - get_block_info_for_utxo(&self.pool, utxo.tx_hash.into()).await?.ok_or( + get_block_info_for_tx_hash(&self.pool, tx_hash.into()).await?.ok_or( format!( "Could not find block info for data checkpoint: {data_checkpoint:?}" ), )?; - ResolvedBridgeDataCheckpoint::Utxo { - block_number, - tx_ix, - tx_out_ix: utxo.index.into(), - } + ResolvedBridgeDataCheckpoint::Tx { block_number, tx_ix } }, BridgeDataCheckpoint::Block(number) => { ResolvedBridgeDataCheckpoint::Block { number: number.into() } }, }; - let utxos = get_bridge_utxos_tx( + let txs = get_bridge_txs( self.db_sync_config.get_tx_in_config().await?, &self.pool, &main_chain_scripts.illiquid_circulation_supply_validator_address.into(), @@ -128,28 +125,26 @@ observed_async_trait!( ) .await?; - let new_checkpoint = match utxos.last() { + let new_checkpoint = match txs.last() { None => BridgeDataCheckpoint::Block(current_mc_block.block_no.into()), - Some(_) if (utxos.len() as u32) < max_transfers => { + Some(_) if (txs.len() as u32) < max_transfers => { BridgeDataCheckpoint::Block(current_mc_block.block_no.into()) }, - Some(utxo) => BridgeDataCheckpoint::Utxo(utxo.utxo_id()), + Some(tx) => BridgeDataCheckpoint::Tx(tx.tx_id()), }; - let transfers = utxos.into_iter().flat_map(utxo_to_transfer).collect(); + let transfers = txs.into_iter().flat_map(tx_to_transfer).collect(); Ok((transfers, new_checkpoint)) } } ); -fn utxo_to_transfer( - utxo: BridgeUtxo, -) -> Option> +fn tx_to_transfer(tx: BridgeTx) -> Option> where RecipientAddress: for<'a> TryFrom<&'a [u8]>, { - let token_delta = utxo.tokens_out.checked_sub(utxo.tokens_in)?; + let token_delta = tx.tokens_out.checked_sub(tx.tokens_in)?; if token_delta.is_zero() { return None; @@ -157,23 +152,21 @@ where let token_amount = token_delta.0 as u64; - let Some(datum) = utxo.datum.clone() else { - return Some(BridgeTransferV1::InvalidTransfer { token_amount, utxo_id: utxo.utxo_id() }); + let Some(metadata) = tx.metadata.clone() else { + return Some(BridgeTransferV1::InvalidTransfer { token_amount, tx_hash: tx.tx_id() }); }; - let transfer = match TokenTransferDatum::try_from(datum.0) { - Ok(TokenTransferDatum::V1(TokenTransferDatumV1::UserTransfer { receiver })) => { + let transfer = match TokenTransferMetadatum::decode(metadata.0) { + Ok(TokenTransferMetadatum::V1(TokenTransferMetadatumV1::UserTransfer { receiver })) => { match RecipientAddress::try_from(receiver.0.as_ref()) { Ok(recipient) => BridgeTransferV1::UserTransfer { token_amount, recipient }, - Err(_) => { - BridgeTransferV1::InvalidTransfer { token_amount, utxo_id: utxo.utxo_id() } - }, + Err(_) => BridgeTransferV1::InvalidTransfer { token_amount, tx_hash: tx.tx_id() }, } }, - Ok(TokenTransferDatum::V1(TokenTransferDatumV1::ReserveTransfer)) => { + Ok(TokenTransferMetadatum::V1(TokenTransferMetadatumV1::ReserveTransfer)) => { BridgeTransferV1::ReserveTransfer { token_amount } }, - Err(_) => BridgeTransferV1::InvalidTransfer { token_amount, utxo_id: utxo.utxo_id() }, + Err(_) => BridgeTransferV1::InvalidTransfer { token_amount, tx_hash: tx.tx_id() }, }; Some(transfer) diff --git a/toolkit/data-sources/db-sync/src/bridge/tests.rs b/toolkit/data-sources/db-sync/src/bridge/tests.rs index 880166b98f..bd40f65b17 100644 --- a/toolkit/data-sources/db-sync/src/bridge/tests.rs +++ b/toolkit/data-sources/db-sync/src/bridge/tests.rs @@ -4,7 +4,7 @@ use hex_literal::hex; use sidechain_domain::byte_string::ByteString; use sidechain_domain::mainchain_epoch::{Duration, MainchainEpochConfig, Timestamp}; use sidechain_domain::{ - AssetName, MainchainAddress, McBlockHash, McBlockNumber, McTxHash, PolicyId, UtxoId, + AssetName, MainchainAddress, McBlockHash, McBlockNumber, McTxHash, PolicyId, }; use sp_partner_chains_bridge::{ BridgeDataCheckpoint, BridgeTransferV1, MainChainScripts, TokenBridgeDataSource, @@ -45,10 +45,6 @@ fn init_ics_tx_hash() -> McTxHash { McTxHash(hex!("c000000000000000000000000000000000000000000000000000000000000001")) } -fn last_ics_init_utxo() -> UtxoId { - UtxoId::new(init_ics_tx_hash().0, 3) -} - fn reserve_transfer() -> BridgeTransferV1 { BridgeTransferV1::::ReserveTransfer { token_amount: 100 } } @@ -74,29 +70,29 @@ fn invalid_transfer_1() -> BridgeTransferV1 { BridgeTransferV1::InvalidTransfer { // invalid transfer consumes utxo from user transfer 2 token_amount: 1000 - 120, - utxo_id: invalid_transfer_1_utxo(), + tx_hash: invalid_transfer_1_tx(), } } // transfer with no datum fn invalid_transfer_2() -> BridgeTransferV1 { - BridgeTransferV1::InvalidTransfer { token_amount: 1000, utxo_id: invalid_transfer_2_utxo() } + BridgeTransferV1::InvalidTransfer { token_amount: 1000, tx_hash: invalid_transfer_2_tx() } } -fn reserve_transfer_utxo() -> UtxoId { - UtxoId::new(hex!("c000000000000000000000000000000000000000000000000000000000000002"), 0) +fn reserve_transfer_tx() -> McTxHash { + McTxHash(hex!("c000000000000000000000000000000000000000000000000000000000000002")) } -fn user_transfer_1_utxo() -> UtxoId { - UtxoId::new(hex!("c000000000000000000000000000000000000000000000000000000000000003"), 0) +fn user_transfer_1_tx() -> McTxHash { + McTxHash(hex!("c000000000000000000000000000000000000000000000000000000000000003")) } -fn invalid_transfer_1_utxo() -> UtxoId { - UtxoId::new(hex!("c000000000000000000000000000000000000000000000000000000000000005"), 0) +fn invalid_transfer_1_tx() -> McTxHash { + McTxHash(hex!("c000000000000000000000000000000000000000000000000000000000000005")) } -fn invalid_transfer_2_utxo() -> UtxoId { - UtxoId::new(hex!("c000000000000000000000000000000000000000000000000000000000000006"), 0) +fn invalid_transfer_2_tx() -> McTxHash { + McTxHash(hex!("c000000000000000000000000000000000000000000000000000000000000006")) } fn main_chain_scripts() -> MainChainScripts { @@ -187,7 +183,7 @@ fn create_cached_source(pool: PgPool) -> CachedTokenBridgeDataSourceImpl { with_migration_versions_and_caching! { async fn gets_transfers_from_init_to_block_2(data_source: &dyn TokenBridgeDataSource) { - let data_checkpoint = BridgeDataCheckpoint::Utxo(last_ics_init_utxo()); + let data_checkpoint = BridgeDataCheckpoint::Tx(init_ics_tx_hash()); let current_mc_block = block_2_hash(); let max_transfers = 2; @@ -199,11 +195,11 @@ with_migration_versions_and_caching! { // There's two transfers done in block 2 assert_eq!(transfers, vec![reserve_transfer(), user_transfer_1()]); - assert_eq!(new_checkpoint, BridgeDataCheckpoint::Utxo(user_transfer_1_utxo())) + assert_eq!(new_checkpoint, BridgeDataCheckpoint::Tx(user_transfer_1_tx())) } async fn gets_transfers_from_init_to_block_4(data_source: &dyn TokenBridgeDataSource) { - let data_checkpoint = BridgeDataCheckpoint::Utxo(last_ics_init_utxo()); + let data_checkpoint = BridgeDataCheckpoint::Tx(init_ics_tx_hash()); let current_mc_block = block_4_hash(); let max_transfers = 5; @@ -218,7 +214,7 @@ with_migration_versions_and_caching! { vec![reserve_transfer(), user_transfer_1(), user_transfer_2(), invalid_transfer_1(), invalid_transfer_2()] ); - assert_eq!(new_checkpoint, BridgeDataCheckpoint::Utxo(invalid_transfer_2_utxo())) + assert_eq!(new_checkpoint, BridgeDataCheckpoint::Tx(invalid_transfer_2_tx())) } async fn accepts_block_checkpoint(data_source: &dyn TokenBridgeDataSource) { @@ -237,7 +233,7 @@ with_migration_versions_and_caching! { vec![reserve_transfer(), user_transfer_1(), user_transfer_2(), invalid_transfer_1(), invalid_transfer_2()] ); - assert_eq!(new_checkpoint, BridgeDataCheckpoint::Utxo(invalid_transfer_2_utxo())) + assert_eq!(new_checkpoint, BridgeDataCheckpoint::Tx(invalid_transfer_2_tx())) } async fn returns_block_checkpoint_when_no_transfers_are_found(data_source: &dyn TokenBridgeDataSource) { @@ -274,7 +270,7 @@ with_migration_versions_and_caching! { } async fn truncates_output_and_returns_utxo_checkpoint_if_max_output_is_reached(data_source: &dyn TokenBridgeDataSource) { - let data_checkpoint = BridgeDataCheckpoint::Utxo(last_ics_init_utxo()); + let data_checkpoint = BridgeDataCheckpoint::Tx(init_ics_tx_hash()); let current_mc_block = block_2_hash(); let max_transfers = 1; @@ -286,7 +282,7 @@ with_migration_versions_and_caching! { // There's two transfers done in block 2 assert_eq!(transfers, vec![reserve_transfer()]); - assert_eq!(new_checkpoint, BridgeDataCheckpoint::Utxo(reserve_transfer_utxo())) + assert_eq!(new_checkpoint, BridgeDataCheckpoint::Tx(reserve_transfer_tx())) } async fn utxos_from_checkpoint_block_are_not_included_in_result(data_source: &dyn TokenBridgeDataSource) { diff --git a/toolkit/data-sources/db-sync/src/db_datum.rs b/toolkit/data-sources/db-sync/src/db_datum.rs index 16f0579299..b7d6229234 100644 --- a/toolkit/data-sources/db-sync/src/db_datum.rs +++ b/toolkit/data-sources/db-sync/src/db_datum.rs @@ -17,10 +17,7 @@ impl sqlx::Type for DbDatum { } } -impl<'r> sqlx::Decode<'r, Postgres> for DbDatum -where - JsonValue: Decode<'r, Postgres>, -{ +impl<'r> sqlx::Decode<'r, Postgres> for DbDatum { fn decode(value: ::ValueRef<'r>) -> Result { let value: JsonValue = >::decode(value)?; let datum = encode_json_value_to_plutus_datum(value, DetailedSchema); diff --git a/toolkit/data-sources/db-sync/src/db_metadatum.rs b/toolkit/data-sources/db-sync/src/db_metadatum.rs new file mode 100644 index 0000000000..ca54deddbe --- /dev/null +++ b/toolkit/data-sources/db-sync/src/db_metadatum.rs @@ -0,0 +1,25 @@ +use cardano_serialization_lib::{ + MetadataJsonSchema, TransactionMetadatum, encode_json_value_to_metadatum, +}; +use sqlx::postgres::PgTypeInfo; +use sqlx::postgres::types::Oid; +use sqlx::types::JsonValue; +use sqlx::{Database, Decode, Postgres, error::BoxDynError}; + +/// Wraps TransactionMetadatum to provide sqlx::Decode and sqlx::Type implementations +#[derive(Debug, Clone, PartialEq)] +pub struct DbMetadatum(pub TransactionMetadatum); + +impl sqlx::Type for DbMetadatum { + fn type_info() -> ::TypeInfo { + PgTypeInfo::with_oid(Oid(3802)) + } +} + +impl<'r> sqlx::Decode<'r, Postgres> for DbMetadatum { + fn decode(value: ::ValueRef<'r>) -> Result { + let value: JsonValue = >::decode(value)?; + let datum = encode_json_value_to_metadatum(value, MetadataJsonSchema::BasicConversions); + Ok(DbMetadatum(datum?)) + } +} diff --git a/toolkit/data-sources/db-sync/src/db_model.rs b/toolkit/data-sources/db-sync/src/db_model.rs index 5e32da3c96..b55389c036 100644 --- a/toolkit/data-sources/db-sync/src/db_model.rs +++ b/toolkit/data-sources/db-sync/src/db_model.rs @@ -1,4 +1,5 @@ use crate::db_datum::DbDatum; +use crate::db_metadatum::DbMetadatum; use crate::{DataSourceError, SqlxError}; use bigdecimal::ToPrimitive; use cardano_serialization_lib::PlutusData; @@ -835,7 +836,7 @@ mod tests { 1, ); - let result = get_block_info_for_utxo(&pool, utxo.tx_hash.into()) + let result = get_block_info_for_tx_hash(&pool, utxo.tx_hash.into()) .await .expect("Should succeed") .expect("Should find data"); @@ -846,33 +847,32 @@ mod tests { #[cfg(feature = "bridge")] #[derive(Debug, Clone, sqlx::FromRow, PartialEq)] -pub(crate) struct BridgeUtxo { +pub(crate) struct BridgeTx { pub(crate) block_number: BlockNumber, pub(crate) tx_ix: TxIndexInBlock, pub(crate) tx_hash: TxHash, - pub(crate) utxo_ix: TxIndex, pub(crate) tokens_out: NativeTokenAmount, pub(crate) tokens_in: NativeTokenAmount, - pub(crate) datum: Option, + pub(crate) metadata: Option, } #[cfg(feature = "bridge")] -impl BridgeUtxo { - pub(crate) fn utxo_id(&self) -> UtxoId { - UtxoId { tx_hash: self.tx_hash.into(), index: self.utxo_ix.into() } +impl BridgeTx { + pub(crate) fn tx_id(&self) -> McTxHash { + self.tx_hash.into() } - pub(crate) fn ordering_key(&self) -> UtxoOrderingKey { - (self.block_number, self.tx_ix, self.utxo_ix) + pub(crate) fn ordering_key(&self) -> TxOrderingKey { + (self.block_number, self.tx_ix) } } #[cfg(feature = "bridge")] -pub(crate) type UtxoOrderingKey = (BlockNumber, TxIndexInBlock, TxIndex); +pub(crate) type TxOrderingKey = (BlockNumber, TxIndexInBlock); #[cfg(feature = "bridge")] #[derive(Debug, PartialEq, Clone, Copy)] pub(crate) enum ResolvedBridgeDataCheckpoint { - Utxo { block_number: BlockNumber, tx_ix: TxIndexInBlock, tx_out_ix: TxIndex }, + Tx { block_number: BlockNumber, tx_ix: TxIndexInBlock }, Block { number: BlockNumber }, } @@ -881,7 +881,7 @@ impl ResolvedBridgeDataCheckpoint { pub(crate) fn get_block_number(&self) -> BlockNumber { match self { ResolvedBridgeDataCheckpoint::Block { number } => *number, - ResolvedBridgeDataCheckpoint::Utxo { block_number, .. } => *block_number, + ResolvedBridgeDataCheckpoint::Tx { block_number, .. } => *block_number, } } } @@ -894,7 +894,7 @@ pub(crate) struct TxBlockInfo { } #[cfg(feature = "bridge")] -pub(crate) async fn get_block_info_for_utxo( +pub(crate) async fn get_block_info_for_tx_hash( pool: &Pool, tx_hash: TxHash, ) -> Result, SqlxError> { @@ -915,78 +915,99 @@ WHERE tx.hash = $1 } #[cfg(feature = "bridge")] -pub(crate) async fn get_bridge_utxos_tx( +pub(crate) async fn get_bridge_txs( tx_in_configuration: TxInConfiguration, pool: &Pool, icp_address: &Address, native_token: Asset, checkpoint: ResolvedBridgeDataCheckpoint, to_block: BlockNumber, - max_utxos: Option, -) -> Result, SqlxError> { + max_transfers: Option, +) -> Result, SqlxError> { use sqlx::QueryBuilder; - let mut query_builder = QueryBuilder::new( - " - SELECT - block.block_no AS block_number - , tx.block_index AS tx_ix - , tx.hash AS tx_hash - , outputs.index AS utxo_ix - , output_tokens.quantity AS tokens_out - , coalesce(sum(input_tokens.quantity), 0) AS tokens_in - , datum.value AS datum - - FROM tx_out outputs - JOIN tx ON outputs.tx_id = tx.id - JOIN block ON tx.block_id = block.id - JOIN ma_tx_out output_tokens ON output_tokens.tx_out_id = outputs.id - JOIN multi_asset native_token ON native_token.id = output_tokens.ident - LEFT JOIN datum ON datum.hash = outputs.data_hash -", - ); + let max_rows = max_transfers.as_ref().map(ToString::to_string).unwrap_or("null".into()); - query_builder.push(match tx_in_configuration { - TxInConfiguration::Consumed => - "LEFT JOIN tx_out inputs ON inputs.consumed_by_tx_id = tx.id AND inputs.address = $1", - TxInConfiguration::Enabled => - "LEFT JOIN tx_in inputs_join ON tx.id = inputs_join.tx_in_id - LEFT JOIN tx_out inputs ON inputs_join.tx_out_id = inputs.tx_id and inputs_join.tx_out_index = inputs.index AND inputs.address = $1", - }); + let checkpoint_limit = match checkpoint { + ResolvedBridgeDataCheckpoint::Block { number } => { + format!("block.block_no > {}", number.0) + }, + ResolvedBridgeDataCheckpoint::Tx { block_number, tx_ix } => { + format!("(block.block_no, tx.block_index) > ({}, {})", block_number.0, tx_ix.0) + }, + }; - query_builder.push( + let transfers_subquery = format!( " - LEFT JOIN ma_tx_out input_tokens ON input_tokens.tx_out_id = inputs.id AND input_tokens.ident = native_token.id - WHERE native_token.policy = $2 - AND native_token.name = $3 - AND outputs.address = $1 - AND block_no <= $4 -", + SELECT + block.block_no AS block_number + , tx.block_index AS tx_ix + , tx.hash AS tx_hash + , tx.id AS tx_id + , tx_metadata.json AS metadata + , coalesce(sum(output_tokens.quantity), 0) AS tokens_out + , native_token.id AS native_token_id + FROM tx_out outputs + JOIN tx ON outputs.tx_id = tx.id + JOIN block ON tx.block_id = block.id + JOIN ma_tx_out output_tokens ON output_tokens.tx_out_id = outputs.id + JOIN multi_asset native_token ON native_token.id = output_tokens.ident + LEFT JOIN tx_metadata ON tx_metadata.tx_id = tx.id + WHERE native_token.policy = $2 + AND native_token.name = $3 + AND outputs.address = $1 + AND block_no <= $4 + AND {checkpoint_limit} + GROUP BY tx.id, tx.hash, tx_metadata.json, block.block_no, tx.block_index, native_token.id + LIMIT {max_rows} +" ); - match checkpoint { - ResolvedBridgeDataCheckpoint::Block { number } => { - query_builder.push(&format!("AND block_no > {} ", number.0)); + let inputs_subquery = match tx_in_configuration { + TxInConfiguration::Consumed => { + " + SELECT + tx_out.consumed_by_tx_id AS consuming_tx_id + , ma_tx_out.ident AS native_token_id + , ma_tx_out.quantity AS tokens_in + FROM tx_out + LEFT JOIN ma_tx_out ON ma_tx_out.tx_out_id = tx_out.id + WHERE tx_out.address = $1 + " }, - ResolvedBridgeDataCheckpoint::Utxo { block_number, tx_ix, tx_out_ix } => { - query_builder.push(&format!( - "AND (block_no, tx.block_index, outputs.index) > ({}, {}, {}) ", - block_number.0, tx_ix.0, tx_out_ix.0 - )); + TxInConfiguration::Enabled => { + " + SELECT + tx_in.tx_in_id AS consuming_tx_id + , ma_tx_out.ident AS native_token_id + , ma_tx_out.quantity AS tokens_in + FROM tx_out + LEFT JOIN ma_tx_out ON ma_tx_out.tx_out_id = tx_out.id + LEFT JOIN tx_in ON tx_in.tx_out_id = tx_out.tx_id and tx_in.tx_out_index = tx_out.index + WHERE tx_out.address = $1 + " }, - } - - query_builder - .push("GROUP BY tx.hash, outputs.id, output_tokens.quantity, datum.value, block.block_no, tx.block_index, outputs.index ") - .push("ORDER BY block.block_no, tx.block_index, outputs.index "); - - if let Some(max_utxos) = max_utxos { - query_builder.push(&format!("LIMIT {max_utxos}")); - } - query_builder.push(";"); + }; + + let mut query_builder = QueryBuilder::new(&format!(" + WITH + transfers AS ({transfers_subquery}) + , inputs AS ({inputs_subquery}) + SELECT + block_number + , tx_ix + , tx_hash + , tokens_out + , coalesce(sum(tokens_in), 0) AS tokens_in + , metadata + FROM transfers + LEFT JOIN inputs ON inputs.consuming_tx_id = transfers.tx_id AND inputs.native_token_id = transfers.native_token_id + GROUP BY tx_hash, metadata, block_number, tx_ix, tokens_out + ORDER BY block_number, tx_ix; + ")); let query = query_builder - .build_query_as::() + .build_query_as::() .bind(&icp_address.0) .bind(&native_token.policy_id.0) .bind(&native_token.asset_name.0) diff --git a/toolkit/data-sources/db-sync/src/lib.rs b/toolkit/data-sources/db-sync/src/lib.rs index 15446f09e4..d39f43e8ec 100644 --- a/toolkit/data-sources/db-sync/src/lib.rs +++ b/toolkit/data-sources/db-sync/src/lib.rs @@ -115,6 +115,7 @@ pub use crate::stake_distribution::StakeDistributionDataSourceImpl; mod data_sources; mod db_datum; +mod db_metadatum; mod db_model; #[cfg(feature = "block-source")] diff --git a/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-consumed/1_create_schema.sql b/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-consumed/1_create_schema.sql index 46d7200adc..d774f29ee6 100644 --- a/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-consumed/1_create_schema.sql +++ b/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-consumed/1_create_schema.sql @@ -204,3 +204,13 @@ CREATE TABLE public.ma_tx_out ( -- CONSTRAINT ma_tx_out_tx_out_id_fkey FOREIGN KEY (tx_out_id) REFERENCES public.tx_out(id) ON DELETE CASCADE ON UPDATE RESTRICT ); CREATE INDEX idx_ma_tx_out_tx_out_id ON public.ma_tx_out USING btree (tx_out_id); + +CREATE TABLE tx_metadata ( + id SERIAL8 PRIMARY KEY UNIQUE, + "key" word64type NOT NULL, + json jsonb NULL, + bytes bytea NOT NULL, + tx_id INT8 NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_tx_metadata_tx_id ON tx_metadata(tx_id) ; diff --git a/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-consumed/6_insert_transactions.sql b/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-consumed/6_insert_transactions.sql index 7c863d4c27..3733848640 100644 --- a/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-consumed/6_insert_transactions.sql +++ b/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-consumed/6_insert_transactions.sql @@ -1,33 +1,9 @@ DO $$ DECLARE - reserve_datum jsonb := '{ - "list": [ - { "constructor": 0, "fields": [] }, - { "constructor": 1, "fields": [] }, - { "int": 1 } - ] - }'; - - transfer_datum_1 jsonb := '{ - "list": [ - { "constructor": 0, "fields": [] }, - { "constructor": 0, "fields": [{ "bytes": "abcd" }] }, - { "int": 1 } - ] - }'; - - transfer_datum_2 jsonb := '{ - "list": [ - { "constructor": 0, "fields": [] }, - { "constructor": 0, "fields": [{ "bytes": "1234" }] }, - { "int": 1 } - ] - }'; - - invalid_datum jsonb := '{ - "list": [ { "int": 42 } ] - }'; - + reserve_datum jsonb := '{ "v": 1, "p": "reserve" }'; + transfer_datum_1 jsonb := '{ "v": 1, "p": { "receiver": "0xabcd" } }'; + transfer_datum_2 jsonb := '{ "v": 1, "p": { "receiver": "0x1234" } }'; + invalid_datum jsonb := '{ "invalid-key": "huh?" }'; native_token_policy hash28type := decode('500000000000000000000000000000000000434845434b504f494e69', 'hex'); native_token_id integer := 1; @@ -50,11 +26,6 @@ DECLARE ivalid_transfer_tx_hash_2 hash32type := decode('c000000000000000000000000000000000000000000000000000000000000006','hex'); irrelevant_tx_hash hash32type := decode('4242424242424242424242424242424242424242424242424242424242424242','hex'); - reserve_transfer_datum_hash hash32type := decode('0000000000000000000000000000000000000000000000000000000000000001','hex'); - user_tranfer_datum_hash_1 hash32type := decode('1000000000000000000000000000000000000000000000000000000000000001','hex'); - user_tranfer_datum_hash_2 hash32type := decode('1000000000000000000000000000000000000000000000000000000000000002','hex'); - invalid_transfer_datum hash32type := decode('1000000000000000000000000000000000000000000000000000000000000003','hex'); - BEGIN INSERT INTO tx ( id , hash , block_id, block_index, out_sum, fee, deposit, size, invalid_before, invalid_hereafter, valid_contract, script_size ) @@ -74,18 +45,18 @@ INSERT INTO tx_out ( id, tx_id , index, address , address_ra ,( 13, init_ics_tx , 2 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL , NULL ) -- ICS initial utxo 3 ,( 14, init_ics_tx , 3 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL , NULL ) -- ICS initial utxo 4 ,( 15, irrelevant_tx , 0 , 'irrelevant' , '' , TRUE , NULL , NULL , 0 , NULL , user_transfer_tx_2 ) -- Irrelevant transaction with some native token - ,( 21, reserve_transfer_tx , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , reserve_transfer_datum_hash , user_transfer_tx_1 ) -- transfers 100 tokens - ,( 31, user_transfer_tx_1 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , user_tranfer_datum_hash_1 , user_transfer_tx_2 ) -- transfers 10 tokens + 100 tokens from previous transaction's utxo, consumes `irrelevant_tx#0` - ,( 32, user_transfer_tx_2 , 1 , 'ics address', '' , TRUE , NULL , NULL , 0 , user_tranfer_datum_hash_2 , invalid_transfer_tx_1 ) -- transfers 10 tokens + 110 tokens from previous transaction's utxo - ,( 41, invalid_transfer_tx_1 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , invalid_transfer_datum , NULL ) -- invalid transfer, with invalid datum + ,( 21, reserve_transfer_tx , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL , user_transfer_tx_1 ) -- transfers 100 tokens + ,( 31, user_transfer_tx_1 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL , user_transfer_tx_2 ) -- transfers 10 tokens + 100 tokens from previous transaction's utxo, consumes `irrelevant_tx#0` + ,( 32, user_transfer_tx_2 , 1 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL , invalid_transfer_tx_1 ) -- transfers 10 tokens + 110 tokens from previous transaction's utxo + ,( 41, invalid_transfer_tx_1 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL , NULL ) -- invalid transfer, with invalid datum ,( 42, invalid_transfer_tx_2 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL , NULL ) -- invalid transfer, no datum ; -INSERT INTO datum ( id, hash , tx_id , value ) - VALUES ( 0 , reserve_transfer_datum_hash , irrelevant_tx , reserve_datum ) - ,( 1 , user_tranfer_datum_hash_1 , irrelevant_tx , transfer_datum_1 ) - ,( 2 , user_tranfer_datum_hash_2 , irrelevant_tx , transfer_datum_2 ) - ,( 3 , invalid_transfer_datum , irrelevant_tx , invalid_datum ) +INSERT INTO tx_metadata ( id , "key" , json , bytes , tx_id ) + VALUES ( 0 , 0 , reserve_datum , '' , reserve_transfer_tx ) + , ( 1 , 1 , transfer_datum_1 , '' , user_transfer_tx_1 ) + , ( 2 , 2 , transfer_datum_2 , '' , user_transfer_tx_2 ) + , ( 3 , 3 , invalid_datum , '' , invalid_transfer_tx_1 ) ; INSERT INTO multi_asset ( id , policy , name , fingerprint ) diff --git a/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-enabled/1_create_schema.sql b/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-enabled/1_create_schema.sql index e432f1bc65..cb3ccee3af 100644 --- a/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-enabled/1_create_schema.sql +++ b/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-enabled/1_create_schema.sql @@ -211,3 +211,13 @@ CREATE TABLE public.ma_tx_out ( -- CONSTRAINT ma_tx_out_tx_out_id_fkey FOREIGN KEY (tx_out_id) REFERENCES public.tx_out(id) ON DELETE CASCADE ON UPDATE RESTRICT ); CREATE INDEX idx_ma_tx_out_tx_out_id ON public.ma_tx_out USING btree (tx_out_id); + +CREATE TABLE tx_metadata ( + id SERIAL8 PRIMARY KEY UNIQUE, + "key" word64type NOT NULL, + json jsonb NULL, + bytes bytea NOT NULL, + tx_id INT8 NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_tx_metadata_tx_id ON tx_metadata(tx_id) ; diff --git a/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-enabled/6_insert_transactions.sql b/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-enabled/6_insert_transactions.sql index ae9276dd11..9dd65a86f4 100644 --- a/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-enabled/6_insert_transactions.sql +++ b/toolkit/data-sources/db-sync/testdata/bridge/migrations-tx-in-enabled/6_insert_transactions.sql @@ -1,32 +1,10 @@ DO $$ DECLARE - reserve_datum jsonb := '{ - "list": [ - { "constructor": 0, "fields": [] }, - { "constructor": 1, "fields": [] }, - { "int": 1 } - ] - }'; + reserve_datum jsonb := '{ "v": 1, "p": "reserve" }'; + transfer_datum_1 jsonb := '{ "v": 1, "p": { "receiver": "0xabcd" } }'; + transfer_datum_2 jsonb := '{ "v": 1, "p": { "receiver": "0x1234" } }'; + invalid_datum jsonb := '{ "invalid-key": "huh?" }'; - transfer_datum_1 jsonb := '{ - "list": [ - { "constructor": 0, "fields": [] }, - { "constructor": 0, "fields": [{ "bytes": "abcd" }] }, - { "int": 1 } - ] - }'; - - transfer_datum_2 jsonb := '{ - "list": [ - { "constructor": 0, "fields": [] }, - { "constructor": 0, "fields": [{ "bytes": "1234" }] }, - { "int": 1 } - ] - }'; - - invalid_datum jsonb := '{ - "list": [ { "int": 42 } ] - }'; native_token_policy hash28type := decode('500000000000000000000000000000000000434845434b504f494e69', 'hex'); @@ -50,11 +28,6 @@ DECLARE ivalid_transfer_tx_hash_2 hash32type := decode('c000000000000000000000000000000000000000000000000000000000000006','hex'); irrelevant_tx_hash hash32type := decode('4242424242424242424242424242424242424242424242424242424242424242','hex'); - reserve_transfer_datum_hash hash32type := decode('0000000000000000000000000000000000000000000000000000000000000001','hex'); - user_tranfer_datum_hash_1 hash32type := decode('1000000000000000000000000000000000000000000000000000000000000001','hex'); - user_tranfer_datum_hash_2 hash32type := decode('1000000000000000000000000000000000000000000000000000000000000002','hex'); - invalid_transfer_datum hash32type := decode('1000000000000000000000000000000000000000000000000000000000000003','hex'); - BEGIN INSERT INTO tx ( id , hash , block_id, block_index, out_sum, fee, deposit, size, invalid_before, invalid_hereafter, valid_contract, script_size ) @@ -63,29 +36,28 @@ INSERT INTO tx ( id , hash , block_id, ,( reserve_transfer_tx , reserve_transfer_tx_hash , 2 , 0 , 0 , 0 , 0 , 1024, NULL , NULL , TRUE , 1024 ) ,( user_transfer_tx_1 , user_transfer_tx_hash_1 , 2 , 1 , 0 , 0 , 0 , 1024, NULL , NULL , TRUE , 1024 ) ,( user_transfer_tx_2 , user_transfer_tx_hash_2 , 4 , 0 , 0 , 0 , 0 , 1024, NULL , NULL , TRUE , 1024 ) - ,( invalid_transfer_tx_1 , ivalid_transfer_tx_hash_1 , 4 , 1 , 0 , 0 , 0 , 1024, NULL , NULL , TRUE , 1024 ) - ,( invalid_transfer_tx_2 , ivalid_transfer_tx_hash_2 , 4 , 2 , 0 , 0 , 0 , 1024, NULL , NULL , TRUE , 1024 ) + ,( invalid_transfer_tx_1 , ivalid_transfer_tx_hash_1 , 4 , 1 , 0 , 0 , 0 , 1024, NULL , NULL , TRUE , 1024 ) ,( invalid_transfer_tx_2 , ivalid_transfer_tx_hash_2 , 4 , 2 , 0 , 0 , 0 , 1024, NULL , NULL , TRUE , 1024 ) ; -INSERT INTO tx_out ( id, tx_id , index, address , address_raw, address_has_script, payment_cred, stake_address_id, value, data_hash ) - VALUES ( 11, init_ics_tx , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- ICS initial utxo 1 - ,( 12, init_ics_tx , 1 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- ICS initial utxo 2 - ,( 13, init_ics_tx , 2 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- ICS initial utxo 3 - ,( 14, init_ics_tx , 3 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- ICS initial utxo 4 - ,( 15, irrelevant_tx , 0 , 'irrelevant' , '' , TRUE , NULL , NULL , 0 , NULL ) -- Irrelevant transaction with some native token - ,( 21, reserve_transfer_tx , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , reserve_transfer_datum_hash ) -- transfers 100 tokens - ,( 31, user_transfer_tx_1 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , user_tranfer_datum_hash_1 ) -- transfers 10 tokens + 100 tokens from previous transaction's utxo - ,( 32, user_transfer_tx_2 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , user_tranfer_datum_hash_2 ) -- transfers 10 tokens + 110 tokens from previous transaction's utxo - ,( 41, invalid_transfer_tx_1 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , invalid_transfer_datum ) -- invalid transfer, invalid datum - ,( 42, invalid_transfer_tx_2 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- invalid transfer, no datum +INSERT INTO tx_out ( id, tx_id , index, address , address_raw, address_has_script, payment_cred, stake_address_id, value, data_hash ) + VALUES ( 11, init_ics_tx , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- ICS initial utxo 1 + ,( 12, init_ics_tx , 1 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- ICS initial utxo 2 + ,( 13, init_ics_tx , 2 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- ICS initial utxo 3 + ,( 14, init_ics_tx , 3 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- ICS initial utxo 4 + ,( 15, irrelevant_tx , 0 , 'irrelevant' , '' , TRUE , NULL , NULL , 0 , NULL ) -- Irrelevant transaction with some native token + ,( 21, reserve_transfer_tx , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- transfers 100 tokens + ,( 31, user_transfer_tx_1 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- transfers 10 tokens + 100 tokens from previous transaction's utxo + ,( 32, user_transfer_tx_2 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- transfers 10 tokens + 110 tokens from previous transaction's utxo + ,( 41, invalid_transfer_tx_1 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- invalid transfer, invalid datum + ,( 42, invalid_transfer_tx_2 , 0 , 'ics address', '' , TRUE , NULL , NULL , 0 , NULL ) -- invalid transfer, no datum ; -INSERT INTO datum ( id, hash , tx_id , value ) - VALUES ( 0 , reserve_transfer_datum_hash , irrelevant_tx , reserve_datum ) - ,( 1 , user_tranfer_datum_hash_1 , irrelevant_tx , transfer_datum_1 ) - ,( 2 , user_tranfer_datum_hash_2 , irrelevant_tx , transfer_datum_2 ) - ,( 3 , invalid_transfer_datum , irrelevant_tx , invalid_datum ) +INSERT INTO tx_metadata ( id , "key" , json , bytes , tx_id ) + VALUES ( 0 , 0 , reserve_datum , '' , reserve_transfer_tx ) + , ( 1 , 1 , transfer_datum_1 , '' , user_transfer_tx_1 ) + , ( 2 , 2 , transfer_datum_2 , '' , user_transfer_tx_2 ) + , ( 3 , 3 , invalid_datum , '' , invalid_transfer_tx_1 ) ; INSERT INTO multi_asset ( id , policy , name , fingerprint ) diff --git a/toolkit/data-sources/dolos/src/bridge.rs b/toolkit/data-sources/dolos/src/bridge.rs index bfd4341c54..fb5397d6b2 100644 --- a/toolkit/data-sources/dolos/src/bridge.rs +++ b/toolkit/data-sources/dolos/src/bridge.rs @@ -1,13 +1,17 @@ use crate::{ Result, - client::{MiniBFClient, api::MiniBFApi, minibf::format_asset_id}, + client::{ + MiniBFClient, api::MiniBFApi, conversions::metadata_from_response, minibf::format_asset_id, + }, }; use blockfrost_openapi::models::{ - tx_content::TxContent, tx_content_output_amount_inner::TxContentOutputAmountInner, - tx_content_utxo::TxContentUtxo, + tx_content::TxContent, tx_content_metadata_inner::TxContentMetadataInner, + tx_content_output_amount_inner::TxContentOutputAmountInner, tx_content_utxo::TxContentUtxo, +}; +use partner_chains_plutus_data::{ + VersionedMetadatum, + bridge::{TokenTransferMetadatum, TokenTransferMetadatumV1}, }; -use cardano_serialization_lib::PlutusData; -use partner_chains_plutus_data::bridge::{TokenTransferDatum, TokenTransferDatumV1}; use sidechain_domain::*; use sp_partner_chains_bridge::{ BridgeDataCheckpoint, BridgeTransferV1, MainChainScripts, TokenBridgeDataSource, @@ -43,18 +47,12 @@ where let current_mc_block = self.client.blocks_by_id(current_mc_block_hash).await?; let data_checkpoint = match data_checkpoint { - BridgeDataCheckpoint::Utxo(utxo) => { + BridgeDataCheckpoint::Tx(tx_hash) => { let TxBlockInfo { block_number, tx_ix } = - get_block_info_for_utxo(&self.client, utxo.tx_hash.into()).await?.ok_or( - format!( - "Could not find block info for data checkpoint: {data_checkpoint:?}" - ), - )?; - ResolvedBridgeDataCheckpoint::Utxo { - block_number, - tx_ix, - tx_out_ix: utxo.index.into(), - } + get_block_info_for_utxo(&self.client, tx_hash).await?.ok_or(format!( + "Could not find block info for data checkpoint: {data_checkpoint:?}" + ))?; + ResolvedBridgeDataCheckpoint::Tx { block_number, tx_ix } }, BridgeDataCheckpoint::Block(number) => { ResolvedBridgeDataCheckpoint::Block { number: number.into() } @@ -83,7 +81,7 @@ where Some(_) if (utxos.len() as u32) < max_transfers => { BridgeDataCheckpoint::Block(current_mc_block_height) }, - Some(utxo) => BridgeDataCheckpoint::Utxo(utxo.utxo_id()), + Some(utxo) => BridgeDataCheckpoint::Tx(utxo.tx_hash), }; let transfers = utxos.into_iter().flat_map(utxo_to_transfer).collect(); @@ -107,22 +105,20 @@ where let token_amount = token_delta as u64; let Some(datum) = utxo.datum.clone() else { - return Some(BridgeTransferV1::InvalidTransfer { token_amount, utxo_id: utxo.utxo_id() }); + return Some(BridgeTransferV1::InvalidTransfer { token_amount, tx_hash: utxo.tx_hash }); }; - let transfer = match TokenTransferDatum::try_from(datum) { - Ok(TokenTransferDatum::V1(TokenTransferDatumV1::UserTransfer { receiver })) => { + let transfer = match TokenTransferMetadatum::decode(datum) { + Ok(TokenTransferMetadatum::V1(TokenTransferMetadatumV1::UserTransfer { receiver })) => { match RecipientAddress::try_from(receiver.0.as_ref()) { Ok(recipient) => BridgeTransferV1::UserTransfer { token_amount, recipient }, - Err(_) => { - BridgeTransferV1::InvalidTransfer { token_amount, utxo_id: utxo.utxo_id() } - }, + Err(_) => BridgeTransferV1::InvalidTransfer { token_amount, tx_hash: utxo.tx_hash }, } }, - Ok(TokenTransferDatum::V1(TokenTransferDatumV1::ReserveTransfer)) => { + Ok(TokenTransferMetadatum::V1(TokenTransferMetadatumV1::ReserveTransfer)) => { BridgeTransferV1::ReserveTransfer { token_amount } }, - Err(_) => BridgeTransferV1::InvalidTransfer { token_amount, utxo_id: utxo.utxo_id() }, + Err(_) => BridgeTransferV1::InvalidTransfer { token_amount, tx_hash: utxo.tx_hash }, }; Some(transfer) @@ -132,23 +128,18 @@ pub(crate) struct BridgeUtxo { pub(crate) block_number: McBlockNumber, pub(crate) tx_ix: McTxIndexInBlock, pub(crate) tx_hash: McTxHash, - pub(crate) utxo_ix: UtxoIndex, pub(crate) tokens_out: NativeTokenAmount, pub(crate) tokens_in: NativeTokenAmount, - pub(crate) datum: Option, + pub(crate) datum: Option, } impl BridgeUtxo { - pub(crate) fn utxo_id(&self) -> UtxoId { - UtxoId { tx_hash: self.tx_hash.into(), index: self.utxo_ix.into() } - } - pub(crate) fn ordering_key(&self) -> UtxoOrderingKey { - (self.block_number, self.tx_ix, self.utxo_ix) + (self.block_number, self.tx_ix) } } -pub(crate) type UtxoOrderingKey = (McBlockNumber, McTxIndexInBlock, UtxoIndex); +pub(crate) type UtxoOrderingKey = (McBlockNumber, McTxIndexInBlock); #[derive(Debug, Clone, PartialEq)] pub(crate) struct TxBlockInfo { @@ -169,14 +160,14 @@ pub(crate) async fn get_block_info_for_utxo( #[derive(Clone)] pub(crate) enum ResolvedBridgeDataCheckpoint { - Utxo { block_number: McBlockNumber, tx_ix: McTxIndexInBlock, tx_out_ix: UtxoIndex }, + Tx { block_number: McBlockNumber, tx_ix: McTxIndexInBlock }, Block { number: McBlockNumber }, } impl ResolvedBridgeDataCheckpoint { fn block_number(&self) -> McBlockNumber { match self { - ResolvedBridgeDataCheckpoint::Utxo { block_number, .. } => *block_number, + ResolvedBridgeDataCheckpoint::Tx { block_number, .. } => *block_number, ResolvedBridgeDataCheckpoint::Block { number } => *number, } } @@ -198,48 +189,53 @@ pub(crate) async fn get_bridge_utxos_tx( let tx_hash = McTxHash::from_hex_unsafe(&a.tx_hash); let utxos = client.transactions_utxos(tx_hash).await?; let tx = client.transaction_by_hash(tx_hash).await?; - Result::Ok(Some((utxos, tx))) + let tx_metadata = + client.transaction_metadata(&McTxHash::from_hex_unsafe(&tx.hash)).await?; + Result::Ok(Some((utxos, tx_metadata, tx))) } else { Result::Ok(None) } }); let mut bridge_utxos = futures::future::try_join_all(futures) .await? - .iter() + .into_iter() .flatten() - .flat_map(|(utxos, tx): &(TxContentUtxo, TxContent)| { - let inputs = utxos.inputs.iter().filter(|i| i.address == icp_address.to_string()); - let outputs = utxos.outputs.iter().filter(|o| o.address == icp_address.to_string()); + .filter(|(_, _, tx)| match checkpoint { + ResolvedBridgeDataCheckpoint::Tx { block_number, tx_ix } + if (tx.block_height, tx.index) <= (block_number.0 as i32, tx_ix.0 as i32) => + { + false + }, + ResolvedBridgeDataCheckpoint::Block { number } + if tx.block_height <= number.0 as i32 => + { + false + }, + _ => true, + }) + .flat_map(|(utxos, tx_metadata, tx): (TxContentUtxo, TxContentMetadataInner, TxContent)| { let native_token = native_token.clone(); - let checkpoint_clone = checkpoint.clone(); - outputs.filter_map(move |output| { - let native_token = native_token.clone(); - let output_tokens = get_all_tokens(&output.amount, &native_token.clone()); - let input_tokens = inputs - .clone() - .map(move |input| get_all_tokens(&input.amount, &native_token.clone())) - .sum(); + let input_tokens = utxos + .inputs + .iter() + .filter(|i| i.address == icp_address.to_string()) + .map(|input| get_all_tokens(&input.amount, &native_token)) + .sum(); + + let output_tokens = utxos + .outputs + .iter() + .filter(|o| o.address == icp_address.to_string()) + .map(|input| get_all_tokens(&input.amount, &native_token)) + .sum(); - match checkpoint_clone { - ResolvedBridgeDataCheckpoint::Utxo { tx_ix, tx_out_ix, .. } - if tx.block_height <= tx_ix.0 as i32 - && output.output_index <= tx_out_ix.0.into() => - { - None - }, - _ => Some(BridgeUtxo { - block_number: McBlockNumber(tx.block_height as u32), - tokens_out: NativeTokenAmount(output_tokens), - tokens_in: NativeTokenAmount(input_tokens), - datum: output - .inline_datum - .clone() - .map(|d| PlutusData::from_hex(&d).expect("valid datum")), - tx_ix: McTxIndexInBlock(tx.index as u32), - tx_hash: McTxHash::from_hex_unsafe(&tx.hash), - utxo_ix: UtxoIndex(output.output_index as u16), - }), - } + Some(BridgeUtxo { + block_number: McBlockNumber(tx.block_height as u32), + tokens_out: NativeTokenAmount(output_tokens), + tokens_in: NativeTokenAmount(input_tokens), + datum: Some(metadata_from_response(tx_metadata).ok()?), + tx_ix: McTxIndexInBlock(tx.index as u32), + tx_hash: McTxHash::from_hex_unsafe(&tx.hash), }) }) .collect::>(); diff --git a/toolkit/data-sources/dolos/src/client/api.rs b/toolkit/data-sources/dolos/src/client/api.rs index 4513e3c418..1d9dec2136 100644 --- a/toolkit/data-sources/dolos/src/client/api.rs +++ b/toolkit/data-sources/dolos/src/client/api.rs @@ -6,7 +6,8 @@ use blockfrost_openapi::models::{ block_content::BlockContent, epoch_param_content::EpochParamContent, epoch_stake_pool_content_inner::EpochStakePoolContentInner, genesis_content::GenesisContent, pool_history_inner::PoolHistoryInner, pool_list_extended_inner::PoolListExtendedInner, - tx_content::TxContent, tx_content_utxo::TxContentUtxo, + tx_content::TxContent, tx_content_metadata_inner::TxContentMetadataInner, + tx_content_utxo::TxContentUtxo, }; use sidechain_domain::*; @@ -148,4 +149,10 @@ pub trait MiniBFApi { /// Return the information about blockchain genesis. async fn genesis(&self) -> Result; + + /// Returns metadata for a transaction + async fn transaction_metadata( + &self, + tx_hash: &McTxHash, + ) -> Result; } diff --git a/toolkit/data-sources/dolos/src/client/conversions.rs b/toolkit/data-sources/dolos/src/client/conversions.rs index a31a21154e..3727f16161 100644 --- a/toolkit/data-sources/dolos/src/client/conversions.rs +++ b/toolkit/data-sources/dolos/src/client/conversions.rs @@ -2,6 +2,15 @@ use crate::{DataSourceError, Result}; use blockfrost_openapi::models::block_content::BlockContent; use sidechain_domain::*; +#[cfg(feature = "bridge")] +use { + blockfrost_openapi::models::{ + tx_content_metadata_inner::TxContentMetadataInner, + tx_content_metadata_inner_json_metadata::TxContentMetadataInnerJsonMetadata, + }, + cardano_serialization_lib::{MetadataMap, TransactionMetadatum}, +}; + pub fn from_block_content(value: BlockContent) -> Result { Ok(MainchainBlock { number: value @@ -20,3 +29,23 @@ pub fn from_block_content(value: BlockContent) -> Result { timestamp: value.time as u64, }) } + +#[cfg(feature = "bridge")] +pub(crate) fn metadata_from_response( + value: TxContentMetadataInner, +) -> Result { + let result = match *value.json_metadata.to_owned() { + TxContentMetadataInnerJsonMetadata::String(s) => TransactionMetadatum::new_text(s)?, + TxContentMetadataInnerJsonMetadata::Object(obj) => { + let mut map = MetadataMap::new(); + for (key, value) in obj { + let value = serde_json::from_value(value)?; + map.insert_str(&key, &value)?; + } + + TransactionMetadatum::new_map(&map) + }, + }; + + Ok(result) +} diff --git a/toolkit/data-sources/dolos/src/client/minibf.rs b/toolkit/data-sources/dolos/src/client/minibf.rs index 910561fde4..24aeffe360 100644 --- a/toolkit/data-sources/dolos/src/client/minibf.rs +++ b/toolkit/data-sources/dolos/src/client/minibf.rs @@ -6,7 +6,8 @@ use blockfrost_openapi::models::{ block_content::BlockContent, epoch_param_content::EpochParamContent, epoch_stake_pool_content_inner::EpochStakePoolContentInner, genesis_content::GenesisContent, pool_history_inner::PoolHistoryInner, pool_list_extended_inner::PoolListExtendedInner, - tx_content::TxContent, tx_content_utxo::TxContentUtxo, + tx_content::TxContent, tx_content_metadata_inner::TxContentMetadataInner, + tx_content_utxo::TxContentUtxo, }; use serde::de::DeserializeOwned; use sidechain_domain::*; @@ -226,6 +227,13 @@ impl MiniBFApi for MiniBFClient { async fn genesis(&self) -> Result { self.request("genesis").await } + + async fn transaction_metadata( + &self, + tx_hash: &McTxHash, + ) -> Result { + self.request(&format!("txs/{tx_hash}/metadata")).await + } } pub fn format_asset_id(asset_id: &AssetId) -> String { diff --git a/toolkit/partner-chains-cli/src/create_chain_spec/mod.rs b/toolkit/partner-chains-cli/src/create_chain_spec/mod.rs index d6c698c07d..03c2addda7 100644 --- a/toolkit/partner-chains-cli/src/create_chain_spec/mod.rs +++ b/toolkit/partner-chains-cli/src/create_chain_spec/mod.rs @@ -220,7 +220,7 @@ impl CreateChainSpecConfig { .illiquid_circulation_supply_validator_address .clone(), }), - initial_checkpoint: Some(self.genesis_utxo), + initial_checkpoint: Some(self.genesis_utxo.tx_hash), _marker: PhantomData, } } diff --git a/toolkit/partner-chains-cli/src/create_chain_spec/tests.rs b/toolkit/partner-chains-cli/src/create_chain_spec/tests.rs index fd14fbe4e9..7d5b0176d8 100644 --- a/toolkit/partner-chains-cli/src/create_chain_spec/tests.rs +++ b/toolkit/partner-chains-cli/src/create_chain_spec/tests.rs @@ -263,7 +263,7 @@ fn generated_chain_spec() -> serde_json::Value { "marker": null, }, "bridge": { - "initialCheckpoint": "0000000000000000000000000000000000000000000000000000000000000000#0", + "initialCheckpoint": "0x0000000000000000000000000000000000000000000000000000000000000000", "mainChainScripts": { "illiquid_circulation_supply_validator_address": "addr_test1wrhvtvx3f0g9wv9rx8kfqc60jva3e07nqujk2cspekv4mqs9rjdvz", "token_asset_name": "0x5043546f6b656e44656d6f", diff --git a/toolkit/smart-contracts/offchain/src/bridge/deposit.rs b/toolkit/smart-contracts/offchain/src/bridge/deposit.rs index dd4ef08452..8ffb44087f 100644 --- a/toolkit/smart-contracts/offchain/src/bridge/deposit.rs +++ b/toolkit/smart-contracts/offchain/src/bridge/deposit.rs @@ -12,12 +12,12 @@ use crate::{ cardano_keys::CardanoPaymentSigningKey, csl::{ CostStore, Costs, MultiAssetExt, OgmiosUtxoExt, TransactionBuilderExt, TransactionContext, - TransactionOutputAmountBuilderExt, get_builder_config, + TransactionOutputAmountBuilderExt, get_builder_config, unit_plutus_data, }, }; use cardano_serialization_lib::{ - Address, AssetName, BigNum, MultiAsset, PlutusData, ScriptHash, Transaction, - TransactionBuilder, TransactionOutputBuilder, TxInputsBuilder, + Address, AssetName, BigNum, JsError, MultiAsset, ScriptHash, Transaction, TransactionBuilder, + TransactionMetadatum, TransactionOutputBuilder, TxInputsBuilder, }; use ogmios_client::{ query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId}, @@ -25,7 +25,10 @@ use ogmios_client::{ transactions::Transactions, types::OgmiosUtxo, }; -use partner_chains_plutus_data::bridge::TokenTransferDatumV1; +use partner_chains_plutus_data::{ + VersionedMetadatum, + bridge::{TokenTransferMetadatum, TokenTransferMetadatumV1}, +}; use sidechain_domain::{AssetId, McTxHash, UtxoId, byte_string::ByteString}; use std::num::NonZero; @@ -99,9 +102,10 @@ fn deposit_only_tx( ctx: &TransactionContext, ) -> anyhow::Result { let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?); + tx_builder.add_metadatum(&0u64.into(), &to_user_transfer_metadatum(pc_address)?); let output_builder = TransactionOutputBuilder::new() .with_address(ics_address) - .with_plutus_data(&to_user_transfer_datum(pc_address)) + .with_plutus_data(&unit_plutus_data()) .next()?; let ma = MultiAsset::new().with_asset_amount(&token_amount.token, token_amount.amount)?; let output = output_builder.with_minimum_ada_and_asset(&ma, ctx)?.build()?; @@ -195,9 +199,10 @@ fn deposit_tx( costs: Costs, ) -> anyhow::Result { let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?); + tx_builder.add_metadatum(&0u64.into(), &to_user_transfer_metadatum(pc_address)?); let output_builder = TransactionOutputBuilder::new() .with_address(ics_address) - .with_plutus_data(&to_user_transfer_datum(pc_address)) + .with_plutus_data(&unit_plutus_data()) .next()?; let mut ma = ics_utxo .to_csl() @@ -237,6 +242,9 @@ fn deposit_tx( Ok(tx_builder.balance_update_and_build(ctx)?) } -fn to_user_transfer_datum(pc_address: &[u8]) -> PlutusData { - TokenTransferDatumV1::UserTransfer { receiver: ByteString(pc_address.to_vec()) }.into() +fn to_user_transfer_metadatum(pc_address: &[u8]) -> Result { + TokenTransferMetadatum::V1(TokenTransferMetadatumV1::UserTransfer { + receiver: ByteString(pc_address.to_vec()), + }) + .encode() } diff --git a/toolkit/smart-contracts/offchain/src/reserve/handover.rs b/toolkit/smart-contracts/offchain/src/reserve/handover.rs index 0dc07b7d93..7d1fbb7011 100644 --- a/toolkit/smart-contracts/offchain/src/reserve/handover.rs +++ b/toolkit/smart-contracts/offchain/src/reserve/handover.rs @@ -27,6 +27,7 @@ use crate::{ csl::{ AssetIdExt, CostStore, Costs, OgmiosUtxoExt, Script, TransactionBuilderExt, TransactionContext, TransactionExt, TransactionOutputAmountBuilderExt, get_builder_config, + unit_plutus_data, }, governance::GovernanceData, multisig::{MultiSigSmartContractResult, submit_or_create_tx_to_sign}, @@ -40,7 +41,9 @@ use ogmios_client::{ transactions::Transactions, types::OgmiosUtxo, }; -use partner_chains_plutus_data::{bridge::TokenTransferDatumV1, reserve::ReserveRedeemer}; +use partner_chains_plutus_data::{ + VersionedMetadatum, bridge::TokenTransferMetadatum, reserve::ReserveRedeemer, +}; use sidechain_domain::UtxoId; /// Spends current UTXO at validator address to illiquid supply validator and burn reserve auth policy token, preventing further operations. @@ -92,6 +95,8 @@ fn build_tx( let reserve_auth_policy_spend_cost = costs.get_one_spend(); + tx_builder.add_metadatum(&0u64.into(), &TokenTransferMetadatum::reserve_v1().encode()?); + // mint goveranance token tx_builder.add_mint_one_script_token_using_reference_script( &governance.policy.script(), @@ -138,9 +143,7 @@ fn illiquid_supply_validator_output( TransactionOutputBuilder::new().with_address(&scripts.validator.address(ctx.network)); if output_value.amount > 0 { let ma = output_value.token.to_multi_asset(output_value.amount)?; - let amount_builder = tx_output_builder - .with_plutus_data(&TokenTransferDatumV1::ReserveTransfer.into()) - .next()?; + let amount_builder = tx_output_builder.with_plutus_data(&unit_plutus_data()).next()?; amount_builder.with_minimum_ada_and_asset(&ma, ctx)?.build() } else { // Smart-contract requires to deposit exactly one UTXO in the illiquid supply validator, diff --git a/toolkit/smart-contracts/offchain/src/reserve/release.rs b/toolkit/smart-contracts/offchain/src/reserve/release.rs index f31728e88f..dccc654b44 100644 --- a/toolkit/smart-contracts/offchain/src/reserve/release.rs +++ b/toolkit/smart-contracts/offchain/src/reserve/release.rs @@ -39,7 +39,9 @@ use ogmios_client::{ query_ledger_state::*, query_network::QueryNetwork, transactions::Transactions, types::OgmiosUtxo, }; -use partner_chains_plutus_data::{bridge::TokenTransferDatumV1, reserve::ReserveRedeemer}; +use partner_chains_plutus_data::{ + VersionedMetadatum, bridge::TokenTransferMetadatum, reserve::ReserveRedeemer, +}; use sidechain_domain::{McTxHash, UtxoId}; use std::num::NonZero; @@ -129,6 +131,8 @@ fn reserve_release_tx( let left_in_reserve = reserve_balance.checked_sub(amount_to_transfer) .ok_or_else(||anyhow!("Not enough funds in the reserve to transfer {amount_to_transfer} tokens (reserve balance: {reserve_balance})"))?; + tx_builder.add_metadatum(&0u64.into(), &TokenTransferMetadatum::reserve_v1().encode()?); + // Additional reference scripts tx_builder.add_script_reference_input( &reserve_data.auth_policy_version_utxo.to_csl_tx_input(), @@ -201,7 +205,7 @@ fn reserve_release_tx( tx_builder.add_output(&{ TransactionOutputBuilder::new() .with_address(&ics_data.scripts.validator.address(ctx.network)) - .with_plutus_data(&TokenTransferDatumV1::ReserveTransfer.into()) + .with_plutus_data(&unit_plutus_data()) .next()? .with_minimum_ada_and_asset(&ics_tokens, ctx)? .build()? diff --git a/toolkit/smart-contracts/plutus-data/src/bridge.rs b/toolkit/smart-contracts/plutus-data/src/bridge.rs index 9ab9b27c58..ef3f5d06bb 100644 --- a/toolkit/smart-contracts/plutus-data/src/bridge.rs +++ b/toolkit/smart-contracts/plutus-data/src/bridge.rs @@ -1,19 +1,54 @@ //! Plutus data types used by the token bridge use crate::*; -use cardano_serialization_lib::{PlutusData, traits::NoneOrEmpty}; +use cardano_serialization_lib::{JsError, MetadataMap, TransactionMetadatum}; use sidechain_domain::byte_string::ByteString; /// Datum containing token transfer data #[derive(Clone, Debug, PartialEq)] -pub enum TokenTransferDatum { +pub enum TokenTransferMetadatum { /// Version 1 - V1(TokenTransferDatumV1), + V1(TokenTransferMetadatumV1), +} + +impl TokenTransferMetadatum { + /// Creates v1 reserve transfer metadatum + pub fn reserve_v1() -> Self { + Self::V1(TokenTransferMetadatumV1::ReserveTransfer) + } + + /// Creates v1 user transfer metadatum + pub fn user_v1(receiver: ByteString) -> Self { + Self::V1(TokenTransferMetadatumV1::UserTransfer { receiver }) + } +} + +impl VersionedMetadatum for TokenTransferMetadatum { + fn decode_version(version: i32, payload: TransactionMetadatum) -> Result { + match version { + 1 => Ok(Self::V1(TokenTransferMetadatumV1::decode_payload(payload)?)), + _ => Err(JsError::from_str(&format!( + "Unsupported TokenTransferMetadatum version {version}" + ))), + } + } + + fn version(&self) -> i32 { + match self { + Self::V1(_) => 1, + } + } + + fn encode_payload(&self) -> Result { + match self { + Self::V1(v1) => v1.encode_payload(), + } + } } /// Datum containing token transfer data, version 1 #[derive(Clone, Debug, PartialEq)] -pub enum TokenTransferDatumV1 { +pub enum TokenTransferMetadatumV1 { /// User-initiated transfer sent to a specific receiver address UserTransfer { /// Receiving address on the Partner Chain @@ -23,99 +58,54 @@ pub enum TokenTransferDatumV1 { ReserveTransfer, } -impl From for PlutusData { - fn from(datum: TokenTransferDatumV1) -> Self { - VersionedGenericDatum { - version: 1, - datum: PlutusData::new_empty_constr_plutus_data(&0u64.into()), - appendix: { - match datum { - TokenTransferDatumV1::UserTransfer { receiver } => { - PlutusData::new_single_value_constr_plutus_data( - &0u64.into(), - &PlutusData::new_bytes(receiver.0), - ) - }, - TokenTransferDatumV1::ReserveTransfer => { - PlutusData::new_empty_constr_plutus_data(&1u64.into()) - }, - } - }, +impl TokenTransferMetadatumV1 { + fn decode_payload(value: TransactionMetadatum) -> Result { + if let Ok("reserve") = value.as_text().as_deref() { + return Ok(Self::ReserveTransfer); } - .into() - } -} -impl From for PlutusData { - fn from(datum: TokenTransferDatum) -> Self { - match datum { - TokenTransferDatum::V1(datum) => datum.into(), - } + Ok(Self::UserTransfer { + receiver: ByteString(value.as_map()?.get_str("receiver")?.as_bytes()?), + }) } -} -impl TryFrom for TokenTransferDatum { - type Error = DataDecodingError; - fn try_from(data: PlutusData) -> Result { - Self::decode(&data) - } -} + fn encode_payload(&self) -> Result { + match self { + TokenTransferMetadatumV1::ReserveTransfer => { + Ok(TransactionMetadatum::new_text("reserve".to_string())?) + }, + TokenTransferMetadatumV1::UserTransfer { receiver } => { + let receiver_label = TransactionMetadatum::new_text("receiver".to_string())?; + let receiver_bytes = TransactionMetadatum::new_bytes(receiver.to_vec())?; -impl VersionedDatum for TokenTransferDatum { - fn decode(data: &PlutusData) -> crate::DecodingResult { - match plutus_data_version_and_payload(data) { - None => Err(decoding_error_and_log(data, "TokenTransferDatum", "unversioned datum")), - Some(VersionedGenericDatum { appendix, version: 1, .. }) => { - decode_v1_token_transfer_datum(&appendix).ok_or_else(|| { - decoding_error_and_log(&appendix, "TokenTransferDatum", "malformed appendix") - }) + let mut map = MetadataMap::new(); + map.insert(&receiver_label, &receiver_bytes); + Ok(TransactionMetadatum::new_map(&map)) }, - Some(_) => Err(decoding_error_and_log(data, "TokenTransferDatum", "invalid version")), } } } -fn decode_v1_token_transfer_datum(appendix: &PlutusData) -> Option { - let constr = appendix.as_constr_plutus_data()?; - let alternative = u64::from(constr.alternative()); - let data = constr.data(); - - match alternative { - 0 if data.len() == 1 => { - let receiver = data.get(0).as_bytes()?.into(); - Some(TokenTransferDatum::V1(TokenTransferDatumV1::UserTransfer { receiver })) - }, - 1 if data.is_none_or_empty() => { - Some(TokenTransferDatum::V1(TokenTransferDatumV1::ReserveTransfer)) - }, - _ => None, - } -} - #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::test_plutus_data; - - fn reserve_transfer_data() -> PlutusData { - test_plutus_data!({ - "list": [ - { "constructor": 0, "fields": [] }, - { "constructor": 1, "fields": [] }, - { "int":1 }, + use crate::test_helpers::test_tx_metadata; + use cardano_serialization_lib::TransactionMetadatum; - ] + fn reserve_transfer_metadata() -> TransactionMetadatum { + test_tx_metadata!({ + "v": 1, + "p": "reserve", }) } - fn user_transfer_data(addr: &[u8]) -> PlutusData { - test_plutus_data!({ - "list": [ - { "constructor": 0, "fields": [] }, - { "constructor": 0, "fields": [{ "bytes": hex::encode(addr) }] }, - { "int":1 }, + fn user_transfer_data(addr: &[u8]) -> TransactionMetadatum { + test_tx_metadata!({ + "v": 1, + "p": { + "receiver": "0x".to_owned() + &hex::encode(addr) + } - ] }) } @@ -126,11 +116,11 @@ mod tests { #[test] fn user_transfer_v1() { - let datum = TokenTransferDatum::decode(&user_transfer_data(&hex!("abcd"))) + let datum = TokenTransferMetadatum::decode(user_transfer_data(&hex!("abcd"))) .expect("Should decode successfully"); assert_eq!( datum, - TokenTransferDatum::V1(TokenTransferDatumV1::UserTransfer { + TokenTransferMetadatum::V1(TokenTransferMetadatumV1::UserTransfer { receiver: ByteString(hex!("abcd").into()) }) ) @@ -138,9 +128,9 @@ mod tests { #[test] fn reserve_transfer_v1() { - let datum = TokenTransferDatum::decode(&reserve_transfer_data()) + let datum = TokenTransferMetadatum::decode(reserve_transfer_metadata()) .expect("Should decode successfully"); - assert_eq!(datum, TokenTransferDatum::V1(TokenTransferDatumV1::ReserveTransfer)) + assert_eq!(datum, TokenTransferMetadatum::V1(TokenTransferMetadatumV1::ReserveTransfer)) } } @@ -151,20 +141,24 @@ mod tests { #[test] fn user_transfer_v1() { - let data: PlutusData = TokenTransferDatum::V1(TokenTransferDatumV1::UserTransfer { - receiver: ByteString::from_hex_unsafe("abcd"), - }) - .into(); + let data: TransactionMetadatum = + TokenTransferMetadatum::V1(TokenTransferMetadatumV1::UserTransfer { + receiver: ByteString::from_hex_unsafe("abcd"), + }) + .encode() + .expect("Should succeed"); assert_eq!(data, user_transfer_data(&hex!("abcd"))) } #[test] fn reserve_transfer_v1() { - let data: PlutusData = - TokenTransferDatum::V1(TokenTransferDatumV1::ReserveTransfer).into(); + let data: TransactionMetadatum = + TokenTransferMetadatum::V1(TokenTransferMetadatumV1::ReserveTransfer) + .encode() + .expect("Should succeed"); - assert_eq!(data, reserve_transfer_data()) + assert_eq!(data, reserve_transfer_metadata()) } } } diff --git a/toolkit/smart-contracts/plutus-data/src/lib.rs b/toolkit/smart-contracts/plutus-data/src/lib.rs index 733c69f6a0..d4797b3282 100644 --- a/toolkit/smart-contracts/plutus-data/src/lib.rs +++ b/toolkit/smart-contracts/plutus-data/src/lib.rs @@ -1,6 +1,8 @@ //! This crate contains datum types used by Plutus smart contracts from [raw_scripts::SCRIPTS]. #![deny(missing_docs)] -use cardano_serialization_lib::{PlutusData, PlutusList}; +use cardano_serialization_lib::{ + Int, JsError, MetadataMap, PlutusData, PlutusList, TransactionMetadatum, +}; pub mod bridge; pub mod candidate_keys; @@ -134,6 +136,42 @@ impl From for PlutusData { } } +/// Trait that provides decoding of versioned transaction metadata +/// +/// Versioned metadata is encoded as a map with two fields: +/// - `v`: version number +/// - `p`: version-specific payload +/// +/// This trait is similar to [VersionedDatum] but simpler because metadata does not need to +/// conform to any Plutus validator logic +pub trait VersionedMetadatum: Sized { + /// Parses versioned metadata + fn decode(data: TransactionMetadatum) -> Result { + let version = data.as_map()?.get_str("v")?.as_int()?.as_i32_or_fail()?; + let payload = data.as_map()?.get_str("p")?; + Self::decode_version(version, payload) + } + + /// Decodes payload for a specific version of the metadata + fn decode_version(version: i32, payload: TransactionMetadatum) -> Result; + + /// Returns the version number of the metadata + fn version(&self) -> i32; + + /// Encodes metadata + fn encode(&self) -> Result { + let mut map = MetadataMap::new(); + + map.insert_str("p", &self.encode_payload()?)?; + map.insert_str("v", &TransactionMetadatum::new_int(&Int::new_i32(self.version())))?; + + Ok(TransactionMetadatum::new_map(&map)) + } + + /// Encodes payload for a specific version of the metadata + fn encode_payload(&self) -> Result; +} + /// Script hash of plutus scripts for encoding into PlutusData #[derive(Clone, Debug, PartialEq)] pub struct ScriptHash([u8; 28]); @@ -180,6 +218,17 @@ pub(crate) mod test_helpers { } pub(crate) use test_plutus_data; + macro_rules! test_tx_metadata { + ($json:tt) => { + cardano_serialization_lib::encode_json_value_to_metadatum( + serde_json::json!($json), + cardano_serialization_lib::MetadataJsonSchema::BasicConversions, + ) + .expect("test data is valid") + }; + } + pub(crate) use test_tx_metadata; + pub(crate) fn json_to_plutus_data(json: serde_json::Value) -> PlutusData { cardano_serialization_lib::encode_json_value_to_plutus_datum( json, diff --git a/toolkit/utils/db-sync-sqlx/Cargo.toml b/toolkit/utils/db-sync-sqlx/Cargo.toml index e450a69ae6..7f1855757e 100644 --- a/toolkit/utils/db-sync-sqlx/Cargo.toml +++ b/toolkit/utils/db-sync-sqlx/Cargo.toml @@ -15,3 +15,4 @@ workspace = true sqlx = { workspace = true } sidechain-domain = { workspace = true, features = ["std"] } num-traits = { workspace = true } +hex = { workspace = true } diff --git a/toolkit/utils/db-sync-sqlx/src/lib.rs b/toolkit/utils/db-sync-sqlx/src/lib.rs index 9fe5866a0a..80f3ebb695 100644 --- a/toolkit/utils/db-sync-sqlx/src/lib.rs +++ b/toolkit/utils/db-sync-sqlx/src/lib.rs @@ -19,6 +19,7 @@ use sqlx::encode::IsNull; use sqlx::error::BoxDynError; use sqlx::postgres::PgTypeInfo; use sqlx::*; +use std::fmt::{Debug, Formatter}; /// Macro to handle numeric types that are non-negative but are stored by Db-Sync using /// signed SQL types. @@ -121,9 +122,15 @@ sqlx_implementations_for_wrapper!(i16, "INT2", TxIndex, UtxoIndex); pub struct TxIndexInBlock(pub u32); sqlx_implementations_for_wrapper!(i32, "INT4", TxIndexInBlock, McTxIndexInBlock); -#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] +#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] pub struct TxHash(pub [u8; 32]); +impl Debug for TxHash { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "TxHash(0x{})", hex::encode(self.0)) + } +} + impl sqlx::Type for TxHash { fn type_info() -> ::TypeInfo { PgTypeInfo::with_name("bytea")