Skip to main content

chio_settle/channel/
dispute.rs

1use chio_core::crypto::PublicKey;
2use serde::{Deserialize, Serialize};
3
4use super::validation::{
5    digest, validate_digest, validate_positive, validate_text, I_JSON_MAX_SAFE_INTEGER,
6};
7use super::{
8    ChannelError, ChannelSignatureV1, SignedChannelStateV1, VerifiedChannelCloseV1,
9    VerifiedChannelStateV1,
10};
11
12pub const CHANNEL_DISPUTE_SCHEMA: &str = "chio.channel.dispute.v1";
13
14const CHANNEL_STATE_CHAIN_DIGEST_DOMAIN: &[u8] = b"chio.channel.state-chain.digest.v1\0";
15const CHANNEL_DISPUTE_ID_DOMAIN: &[u8] = b"chio.channel.dispute.id.v1\0";
16const CHANNEL_DISPUTE_DIGEST_DOMAIN: &[u8] = b"chio.channel.dispute.digest.v1\0";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase", deny_unknown_fields)]
20pub struct ChannelStateChainProofV1 {
21    pub base_state_digest: String,
22    pub states: Vec<SignedChannelStateV1>,
23}
24
25impl ChannelStateChainProofV1 {
26    pub fn digest(&self) -> Result<String, ChannelError> {
27        validate_digest("chain_base_state_digest", &self.base_state_digest)?;
28        if self.states.is_empty() {
29            return Err(ChannelError::InvalidField("channel_state_chain"));
30        }
31        digest(CHANNEL_STATE_CHAIN_DIGEST_DOMAIN, self)
32    }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct VerifiedChannelStateChainV1 {
37    proof: ChannelStateChainProofV1,
38    terminal_state: VerifiedChannelStateV1,
39}
40
41impl VerifiedChannelStateChainV1 {
42    #[must_use]
43    pub const fn proof(&self) -> &ChannelStateChainProofV1 {
44        &self.proof
45    }
46
47    #[must_use]
48    pub const fn terminal_state(&self) -> &VerifiedChannelStateV1 {
49        &self.terminal_state
50    }
51}
52
53pub fn build_channel_state_chain(
54    base: &VerifiedChannelStateV1,
55    descendants: &[VerifiedChannelStateV1],
56) -> Result<VerifiedChannelStateChainV1, ChannelError> {
57    if descendants.is_empty() {
58        return Err(ChannelError::InvalidField("channel_state_chain"));
59    }
60    let base_state_digest = base.digest()?;
61    let mut prior_state_digest = base_state_digest.clone();
62    let mut prior_channel_id = base.body().channel_id.clone();
63    let mut prior_sequence = base.body().seq;
64    let mut states = Vec::with_capacity(descendants.len());
65    for descendant in descendants {
66        let body = descendant.body();
67        let expected_sequence = prior_sequence
68            .checked_add(1)
69            .filter(|sequence| *sequence <= I_JSON_MAX_SAFE_INTEGER)
70            .ok_or(ChannelError::ArithmeticOverflow)?;
71        let signature = descendant
72            .payee_signature()
73            .ok_or(ChannelError::AuthorityVerification)?;
74        if body.channel_id != prior_channel_id
75            || body.seq != expected_sequence
76            || body.prev_state_digest.as_deref() != Some(&prior_state_digest)
77        {
78            return Err(ChannelError::AuthorityVerification);
79        }
80        states.push(SignedChannelStateV1 {
81            body: body.clone(),
82            payee_signature: signature.clone(),
83        });
84        prior_state_digest = descendant.digest()?;
85        prior_channel_id.clone_from(&body.channel_id);
86        prior_sequence = body.seq;
87    }
88    let proof = ChannelStateChainProofV1 {
89        base_state_digest,
90        states,
91    };
92    proof.digest()?;
93    let terminal_state = descendants
94        .last()
95        .cloned()
96        .ok_or(ChannelError::InvalidField("channel_state_chain"))?;
97    Ok(VerifiedChannelStateChainV1 {
98        proof,
99        terminal_state,
100    })
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase", deny_unknown_fields)]
105pub struct ChannelDisputeBodyV1 {
106    pub schema: String,
107    pub dispute_id: String,
108    pub channel_id: String,
109    pub close_digest: String,
110    pub close_state_digest: String,
111    pub close_state_sequence: u64,
112    pub competing_state_digest: String,
113    pub competing_state_sequence: u64,
114    pub state_chain_proof_digest: String,
115    pub reason: String,
116    pub submitted_at_unix_ms: u64,
117}
118
119impl ChannelDisputeBodyV1 {
120    pub fn validate(&self) -> Result<(), ChannelError> {
121        if self.schema != CHANNEL_DISPUTE_SCHEMA {
122            return Err(ChannelError::InvalidField("channel_dispute_schema"));
123        }
124        for (field, value) in [
125            ("channel_dispute_id", &self.dispute_id),
126            ("dispute_channel_id", &self.channel_id),
127            ("dispute_close_digest", &self.close_digest),
128            ("dispute_close_state_digest", &self.close_state_digest),
129            (
130                "dispute_competing_state_digest",
131                &self.competing_state_digest,
132            ),
133            (
134                "dispute_state_chain_proof_digest",
135                &self.state_chain_proof_digest,
136            ),
137        ] {
138            validate_digest(field, value)?;
139        }
140        validate_text("channel_dispute_reason", &self.reason)?;
141        validate_positive("channel_dispute_submitted_at", self.submitted_at_unix_ms)?;
142        if self.close_state_sequence > I_JSON_MAX_SAFE_INTEGER
143            || self.competing_state_sequence > I_JSON_MAX_SAFE_INTEGER
144            || self.competing_state_sequence <= self.close_state_sequence
145        {
146            return Err(ChannelError::InvalidField("channel_dispute_sequence"));
147        }
148        Ok(())
149    }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase", deny_unknown_fields)]
154pub struct SignedChannelDisputeV1 {
155    pub body: ChannelDisputeBodyV1,
156    pub submitter_signature: ChannelSignatureV1,
157}
158
159impl SignedChannelDisputeV1 {
160    pub fn digest(&self) -> Result<String, ChannelError> {
161        self.body.validate()?;
162        digest(CHANNEL_DISPUTE_DIGEST_DOMAIN, self)
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct ChannelDisputeSubmitterV1 {
168    pub submitter_id: String,
169    pub submitter_key_epoch: u64,
170    pub submitter_key: PublicKey,
171    pub trusted_time_unix_ms: u64,
172}
173
174impl ChannelDisputeSubmitterV1 {
175    fn validate(&self) -> Result<(), ChannelError> {
176        validate_text("channel_dispute_submitter_id", &self.submitter_id)?;
177        validate_positive(
178            "channel_dispute_submitter_key_epoch",
179            self.submitter_key_epoch,
180        )?;
181        validate_positive("channel_dispute_trusted_time", self.trusted_time_unix_ms)
182    }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct VerifiedChannelDisputeV1 {
187    dispute: SignedChannelDisputeV1,
188    chain: VerifiedChannelStateChainV1,
189}
190
191impl VerifiedChannelDisputeV1 {
192    #[must_use]
193    pub const fn artifact(&self) -> &SignedChannelDisputeV1 {
194        &self.dispute
195    }
196
197    #[must_use]
198    pub const fn chain(&self) -> &VerifiedChannelStateChainV1 {
199        &self.chain
200    }
201}
202
203pub fn derive_channel_dispute_id(
204    close_digest: &str,
205    competing_state_digest: &str,
206) -> Result<String, ChannelError> {
207    validate_digest("dispute_close_digest", close_digest)?;
208    validate_digest("dispute_competing_state_digest", competing_state_digest)?;
209    digest(
210        CHANNEL_DISPUTE_ID_DOMAIN,
211        &(close_digest, competing_state_digest),
212    )
213}
214
215pub fn build_channel_dispute_body(
216    close: &VerifiedChannelCloseV1,
217    chain: &VerifiedChannelStateChainV1,
218    reason: String,
219    submitted_at_unix_ms: u64,
220) -> Result<ChannelDisputeBodyV1, ChannelError> {
221    let close_digest = close.artifact().digest()?;
222    let competing_state_digest = chain.terminal_state().digest()?;
223    let competing_state = chain.terminal_state().body();
224    let body = ChannelDisputeBodyV1 {
225        schema: CHANNEL_DISPUTE_SCHEMA.to_owned(),
226        dispute_id: derive_channel_dispute_id(&close_digest, &competing_state_digest)?,
227        channel_id: close.artifact().body.channel_id.clone(),
228        close_digest,
229        close_state_digest: close.artifact().body.final_state_digest.clone(),
230        close_state_sequence: close.artifact().body.final_state_sequence,
231        competing_state_digest,
232        competing_state_sequence: competing_state.seq,
233        state_chain_proof_digest: chain.proof().digest()?,
234        reason,
235        submitted_at_unix_ms,
236    };
237    if chain.proof().base_state_digest != body.close_state_digest {
238        return Err(ChannelError::AuthorityVerification);
239    }
240    body.validate()?;
241    Ok(body)
242}
243
244pub fn verify_channel_dispute(
245    dispute: &SignedChannelDisputeV1,
246    close: &VerifiedChannelCloseV1,
247    chain: &VerifiedChannelStateChainV1,
248    submitter: &ChannelDisputeSubmitterV1,
249) -> Result<VerifiedChannelDisputeV1, ChannelError> {
250    dispute.body.validate()?;
251    submitter.validate()?;
252    dispute.submitter_signature.verify(
253        &dispute.body,
254        &submitter.submitter_id,
255        submitter.submitter_key_epoch,
256        &submitter.submitter_key,
257    )?;
258    let expected = build_channel_dispute_body(
259        close,
260        chain,
261        dispute.body.reason.clone(),
262        dispute.body.submitted_at_unix_ms,
263    )?;
264    if dispute.body != expected
265        || dispute.body.submitted_at_unix_ms > submitter.trusted_time_unix_ms
266        || submitter.trusted_time_unix_ms >= close.artifact().body.dispute_deadline_unix_ms
267    {
268        return Err(ChannelError::AuthorityVerification);
269    }
270    Ok(VerifiedChannelDisputeV1 {
271        dispute: dispute.clone(),
272        chain: chain.clone(),
273    })
274}