1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// Copyright 2026 The Tari Project
// SPDX-License-Identifier: BSD-3-Clause
use tari_engine_types::crypto::validate_covenant_balance_proof;
use tari_template_lib::types::{
Amount,
Hash32,
crypto::PedersenCommitmentBytes,
stealth::{
CovenantBalanceClaim,
StealthInputView,
StealthOutputView,
StealthTransferStatement,
has_output_to,
outputs_preserve_condition,
},
};
/// The data a spend-script predicate can introspect, derived once from the spending `StealthTransferStatement` before
/// the predicate runs. Only commitments, the output authorisations (`spend_key`/`condition_root`),
/// `minimum_value_promise` and tags are exposed — never confidential values.
#[derive(Debug, Clone)]
pub(crate) struct SpendScriptExecution {
pub inputs: Vec<StealthInputView>,
pub outputs: Vec<StealthOutputView>,
/// The committed `condition_root` of each input being spent via the script path, parallel to `inputs`; `None` for
/// key-path inputs (which never participate in a covenant partition). Used to partition inputs by `condition_root`
/// for covenant balance checks; not exposed to the predicate (only output roots are visible).
pub input_condition_roots: Vec<Option<Hash32>>,
pub revealed_input_amount: Amount,
pub revealed_output_amount: Amount,
pub current_input_index: u32,
pub current_input_commitment: PedersenCommitmentBytes,
/// The committed `condition_root` of the UTXO whose leaf is currently executing. Keys the covenant partition.
pub current_input_condition_root: Hash32,
/// The covenant sub-balance proofs supplied by the spender, matched by partition index when the predicate invokes
/// `AssertCovenantBalanced`.
pub covenant_claims: Vec<CovenantBalanceClaim>,
/// The raw spender-supplied witness `data` blob for the invoking input, exposed to a `TemplateFunction` predicate
/// via `SpendContext::data`.
pub witness_data: Vec<u8>,
}
impl SpendScriptExecution {
/// Derives the introspection context for the predicate gating `input_index`. `input_condition_roots` holds the
/// committed root of every script-path input (parallel to the statement's inputs, `None` for key-path inputs);
/// `current_input_condition_root` is the root of the UTXO whose leaf is being invoked.
pub fn new(
statement: &StealthTransferStatement,
input_condition_roots: &[Option<Hash32>],
input_index: u32,
input_commitment: PedersenCommitmentBytes,
current_input_condition_root: Hash32,
witness_data: Vec<u8>,
) -> Self {
Self {
inputs: statement
.inputs_statement
.inputs
.iter()
.map(|i| StealthInputView {
commitment: i.commitment,
})
.collect(),
outputs: statement
.outputs_statement
.outputs
.iter()
.map(|o| StealthOutputView {
commitment: o.output.commitment,
minimum_value_promise: o.output.minimum_value_promise,
auth: o.auth.clone(),
tag: o.tag,
})
.collect(),
input_condition_roots: input_condition_roots.to_vec(),
revealed_input_amount: statement.inputs_statement.revealed_amount,
revealed_output_amount: statement.outputs_statement.revealed_output_amount,
current_input_index: input_index,
current_input_commitment: input_commitment,
current_input_condition_root,
covenant_claims: statement.covenant_claims.clone(),
witness_data,
}
}
/// "Stay in the vault": every stealth output is re-locked under exactly the invoking `condition_root` and nothing
/// else, and there is at least one output. The authorisation must be `Script(root)` with no key path — a
/// `KeyAndScript` output carrying the same root would be key-spendable next block, escaping the covenant, so it is
/// not a preserving output. Bounds only the surviving outputs' authorisation, not the revealed amount.
pub(crate) fn output_preserves_condition(&self) -> bool {
outputs_preserve_condition(&self.outputs, &self.current_input_condition_root)
}
/// At least one stealth output is authorised by exactly `Script(condition_root)` (no key-path escape) and promises
/// at least `min_value`.
pub(crate) fn has_output_to(&self, condition_root: &Hash32, min_value: u64) -> bool {
has_output_to(&self.outputs, condition_root, min_value)
}
/// Verifies the covenant sub-balance proof for the invoking partition (keyed by `current_input_condition_root`),
/// returning whether its value is conserved up to a cleartext outflow of at most `max_revealed`.
///
/// The partition is every input and output sharing that root. A claim is matched by the index of its partition's
/// first input — no root is compared across the claim boundary; the proof signature binds the partition. A missing
/// claim, an outflow over the allowance, or an invalid proof all yield `false`.
pub(crate) fn covenant_balanced(&self, max_revealed: u64) -> bool {
let me = self.current_input_condition_root;
let Some(first_input_index) = self.input_condition_roots.iter().position(|root| *root == Some(me)) else {
return false;
};
let Some(claim) = self
.covenant_claims
.iter()
.find(|claim| claim.partition_input_index as usize == first_input_index)
else {
return false;
};
if claim.revealed_amount > Amount::from_u64(max_revealed) {
return false;
}
let input_commitments = self
.inputs
.iter()
.zip(&self.input_condition_roots)
.filter(|(_, root)| **root == Some(me))
.map(|(input, _)| input.commitment)
.collect::<Vec<_>>();
// Only outputs re-locked under exactly `Script(me)` stay in the partition; a `KeyAndScript` output committing
// the same root carries a key-path escape, so its value is not conserved within the covenant (see
// `StealthOutputView::is_locked_under`).
let output_commitments = self
.outputs
.iter()
.filter(|output| output.is_locked_under(&me))
.map(|output| output.commitment)
.collect::<Vec<_>>();
validate_covenant_balance_proof(
&me,
claim.revealed_amount,
&input_commitments,
&output_commitments,
&claim.signature,
)
}
}