Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions crates/cardano/src/indexes/delta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ impl CardanoIndexDeltaBuilder {
}
}

// Reference script tag
if let Some(script_ref) = output.script_ref() {
tags.push(Tag::new(
utxo::SCRIPT_REF,
pallas_extras::script_ref_hash(&script_ref).to_vec(),
));
}

tags
}

Expand Down Expand Up @@ -461,6 +469,44 @@ mod tests {
assert_eq!(delta.archive[0].tags.len(), 3);
}

/// An output that carries a reference script gets a `script_ref` tag whose
/// key is the script's on-chain hash.
#[test]
fn reference_script_output_gets_script_ref_tag() {
use pallas::codec::minicbor;
use pallas::codec::utils::{CborWrap, KeepRaw};
use pallas::crypto::hash::Hasher;
use pallas::ledger::primitives::conway::{PostAlonzoTransactionOutput, ScriptRef, Value};
use pallas::ledger::traverse::{Era, MultiEraOutput};

let script = pallas::ledger::primitives::alonzo::NativeScript::InvalidHereafter(500_000);

let output = PostAlonzoTransactionOutput {
address: test_shelley_address().to_vec().into(),
value: Value::Coin(1_000_000),
datum_option: None,
script_ref: Some(CborWrap(ScriptRef::NativeScript(KeepRaw::from(
script.clone(),
)))),
};

let cbor = minicbor::to_vec(&output).unwrap();
let output = MultiEraOutput::decode(Era::Conway, &cbor).unwrap();

let tags = CardanoIndexDeltaBuilder::extract_utxo_tags(&output);

// Native scripts hash their CBOR behind a leading 0x00 language tag.
let script_cbor = minicbor::to_vec(&script).unwrap();
let expected = Hasher::<224>::hash_tagged(&script_cbor, 0);

let tag = tags
.iter()
.find(|tag| tag.dimension == utxo::SCRIPT_REF)
.expect("output with a reference script must produce a script_ref tag");

assert_eq!(tag.key, expected.to_vec());
}

/// Drive every tag-producing method on the builder once.
///
/// This is the producer side of the registry: what a block turns into.
Expand Down
3 changes: 3 additions & 0 deletions crates/cardano/src/indexes/dimensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub mod utxo {

/// Native asset subject (policy ID + asset name)
pub const ASSET: TagDimension = "asset";

/// Hash of the reference script carried by the output
pub const SCRIPT_REF: TagDimension = "script_ref";
}

/// Declare a dimension registry: the constants and the `ALL` list that has to
Expand Down
5 changes: 5 additions & 0 deletions crates/cardano/src/indexes/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ pub trait CardanoIndexExt: IndexStore {
self.utxos_by_tag(utxo::ASSET, asset)
}

/// Get UTxOs that carry a script as their reference script.
fn utxos_by_script_ref(&self, script_hash: &[u8]) -> Result<UtxoSet, IndexError> {
self.utxos_by_tag(utxo::SCRIPT_REF, script_hash)
}

// ============ Archive Slot Queries ============

/// Iterate over slots of blocks containing transactions involving an
Expand Down
17 changes: 15 additions & 2 deletions crates/cardano/src/pallas_extras.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ use pallas::ledger::addresses::{
};
use pallas::ledger::primitives::alonzo::MoveInstantaneousReward;
use pallas::ledger::primitives::conway::{
CostModels, DRep, DRepVotingThresholds, PoolVotingThresholds,
CostModels, DRep, DRepVotingThresholds, PoolVotingThresholds, ScriptRef,
};
use pallas::ledger::primitives::{
alonzo::Certificate as AlonzoCert, conway::Certificate as ConwayCert, PoolMetadata,
RationalNumber, Relay, StakeCredential,
};
use pallas::ledger::primitives::{Epoch, ExUnitPrices, ExUnits, Nonce, NonceVariant};
use pallas::ledger::traverse::{MultiEraCert, MultiEraTx};
use pallas::ledger::traverse::{ComputeHash, MultiEraCert, MultiEraTx, OriginalHash};
use serde::{Deserialize, Serialize};

use crate::eras::ChainSummary;
Expand Down Expand Up @@ -436,6 +436,19 @@ pub fn default_cost_models() -> CostModels {
}
}

/// Compute the on-chain script hash of a reference script.
///
/// Each language hashes its own tagged serialization, so the match must stay
/// per-variant instead of hashing the raw bytes once.
pub fn script_ref_hash(script_ref: &ScriptRef) -> Hash<28> {
match script_ref {
ScriptRef::NativeScript(x) => x.original_hash(),
ScriptRef::PlutusV1Script(x) => x.compute_hash(),
ScriptRef::PlutusV2Script(x) => x.compute_hash(),
ScriptRef::PlutusV3Script(x) => x.compute_hash(),
}
}

pub const DREP_KEY_PREFIX: u8 = 0b00100010;
pub const DREP_SCRIPT_PREFIX: u8 = 0b00100011;

Expand Down
4 changes: 4 additions & 0 deletions crates/minibf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ where
"/scripts/{script_hash}/cbor",
get(routes::scripts::by_hash_cbor::<D>),
)
.route(
"/scripts/{script_hash}/utxos",
get(routes::scripts::by_hash_utxos::<D>),
)
.route(
"/scripts/datum/{datum_hash}",
get(routes::scripts::by_datum_hash::<D>),
Expand Down
61 changes: 52 additions & 9 deletions crates/minibf/src/mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use blockfrost_openapi::models::{
block_content_addresses_inner::BlockContentAddressesInner,
block_content_addresses_inner_transactions_inner::BlockContentAddressesInnerTransactionsInner,
block_content_txs_cbor_inner::BlockContentTxsCborInner,
script_utxos_inner::ScriptUtxosInner,
tx_content::TxContent,
tx_content_cbor::TxContentCbor,
tx_content_delegations_inner::TxContentDelegationsInner,
Expand Down Expand Up @@ -431,15 +432,7 @@ impl<'a> IntoModel<String> for ScriptRef<'a> {
type SortKey = ();

fn into_model(self) -> Result<String, StatusCode> {
let out = match self {
ScriptRef::NativeScript(x) => x.original_hash(),
ScriptRef::PlutusV1Script(x) => x.compute_hash(),
ScriptRef::PlutusV2Script(x) => x.compute_hash(),
ScriptRef::PlutusV3Script(x) => x.compute_hash(),
}
.to_string();

Ok(out)
Ok(pallas_extras::script_ref_hash(&self).to_string())
}
}

Expand Down Expand Up @@ -583,6 +576,56 @@ impl<'a> IntoModel<AddressUtxoContentInner> for UtxoOutputModelBuilder<'a> {
Ok(out)
}
}

impl<'a> IntoModel<ScriptUtxosInner> for UtxoOutputModelBuilder<'a> {
type SortKey = (u64, usize, u32);

fn sort_key(&self) -> Option<Self::SortKey> {
self.block_data
.as_ref()
.map(|data| (data.slot, data.tx_index, self.txo_ref.1))
}

fn into_model(self) -> Result<ScriptUtxosInner, StatusCode> {
let out = ScriptUtxosInner {
address: self.output.address().into_model()?,
// source the tx_hash from the UTxO's own TxoRef (always present),
// not the archive block_data lookup which is None for blocks older
// than `max_history` and would yield an empty hash.
tx_hash: self.txo_ref.0.to_string(),
block: self
.block_data
.as_ref()
.map(|b| b.hash.to_string())
.unwrap_or_default(),
output_index: try_into_or_500!(self.txo_ref.1),
amount: self.output.value().into_model()?,
data_hash: self.output.datum().map(|x| match x {
DatumOption::Hash(x) => x.to_string(),
DatumOption::Data(x) => x.original_hash().to_string(),
}),
inline_datum: self
.output
.datum()
.and_then(|x| match x {
DatumOption::Hash(_) => None,
DatumOption::Data(x) => Some(minicbor::to_vec(&x.0).unwrap()),
})
.map(hex::encode),
// the model requires the hash; callers only build this model for
// outputs that carry a reference script.
reference_script_hash: self
.output
.script_ref()
.map(|h| h.into_model())
.transpose()?
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?,
};

Ok(out)
}
}

pub struct UtxoInputModelBuilder<'a> {
input: MultiEraInput<'a>,
as_output: Option<MultiEraOutput<'a>>,
Expand Down
3 changes: 2 additions & 1 deletion crates/minibf/src/routes/addresses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ where
return Err(Error::Code(StatusCode::NOT_FOUND));
}

let mut utxos = super::utxos::load_utxo_models(&domain, refs, pagination).await?;
let mut utxos: Vec<AddressUtxoContentInner> =
super::utxos::load_utxo_models(&domain, refs, pagination).await?;

if should_filter {
utxos.retain(|x| x.amount.iter().all(|x| x.unit == "lovelace"));
Expand Down
Loading
Loading