Skip to main content

wavs_types/
signing.rs

1cfg_if::cfg_if! {
2    if #[cfg(feature = "signer")] {
3        pub mod signer;
4        pub use signer::*;
5    }
6}
7
8pub use crate::solidity_types::Envelope;
9use crate::{
10    ServiceId, ServiceManagerEnvelope, ServiceManagerSignatureData, SignatureData, SignatureKind,
11    SubmitAction, TriggerAction, TriggerData, WasmResponse, WorkflowId,
12};
13use alloy_primitives::{eip191_hash_message, keccak256, FixedBytes, SignatureError};
14use alloy_sol_types::SolValue;
15use async_trait::async_trait;
16use ripemd::Ripemd160;
17use serde::{Deserialize, Serialize};
18use sha2::Digest;
19use thiserror::Error;
20use utoipa::ToSchema;
21
22#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
23#[serde(rename_all = "snake_case")]
24pub struct AggregatorInput {
25    pub trigger_action: TriggerAction,
26    pub operator_response: WasmResponse,
27}
28
29impl AggregatorInput {
30    pub fn event_id(&self) -> Result<EventId, bincode::error::EncodeError> {
31        let salt = match &self.operator_response.event_id_salt {
32            Some(bytes) => EventIdSalt::WasmResponse(bytes),
33            None => EventIdSalt::Trigger(&self.trigger_action.data),
34        };
35
36        EventId::new(
37            &self.trigger_action.config.service_id,
38            &self.trigger_action.config.workflow_id,
39            salt,
40        )
41    }
42}
43#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
44#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
45pub trait WavsSignable {
46    // just need to impl this
47    fn encode_data(&self) -> anyhow::Result<Vec<u8>>;
48
49    fn prefix_eip191_hash(&self) -> anyhow::Result<FixedBytes<32>> {
50        let envelope_bytes = self.encode_data()?;
51        Ok(eip191_hash_message(keccak256(&envelope_bytes)))
52    }
53
54    fn unprefixed_hash(&self) -> anyhow::Result<FixedBytes<32>> {
55        let envelope_bytes = self.encode_data()?;
56        Ok(keccak256(&envelope_bytes))
57    }
58}
59
60impl WavsSignable for Envelope {
61    fn encode_data(&self) -> anyhow::Result<Vec<u8>> {
62        Ok(self.abi_encode())
63    }
64}
65
66impl WavsSignable for &[SubmitAction] {
67    fn encode_data(&self) -> anyhow::Result<Vec<u8>> {
68        Ok(bincode::serde::encode_to_vec(
69            self,
70            bincode::config::standard(),
71        )?)
72    }
73}
74
75impl From<Envelope> for ServiceManagerEnvelope {
76    fn from(envelope: Envelope) -> Self {
77        ServiceManagerEnvelope {
78            eventId: envelope.eventId,
79            ordering: envelope.ordering,
80            payload: envelope.payload,
81        }
82    }
83}
84
85impl From<SignatureData> for ServiceManagerSignatureData {
86    fn from(signature_data: SignatureData) -> Self {
87        ServiceManagerSignatureData {
88            signers: signature_data.signers,
89            signatures: signature_data.signatures,
90            referenceBlock: signature_data.referenceBlock,
91        }
92    }
93}
94
95#[derive(Serialize, Deserialize, Clone, Debug, ToSchema, PartialEq, Eq, Hash)]
96#[serde(rename_all = "snake_case")]
97pub struct WavsSignature {
98    pub data: Vec<u8>,
99    pub kind: SignatureKind,
100}
101
102#[derive(
103    Serialize,
104    Deserialize,
105    Clone,
106    Eq,
107    PartialEq,
108    Debug,
109    Hash,
110    bincode::Decode,
111    bincode::Encode,
112    Ord,
113    PartialOrd,
114    Default,
115)]
116#[serde(transparent)]
117pub struct EventId(#[serde(with = "const_hex")] [u8; 20]);
118
119impl EventId {
120    pub fn new(
121        service_id: &ServiceId,
122        workflow_id: &WorkflowId,
123        salt: EventIdSalt<'_>,
124    ) -> Result<Self, bincode::error::EncodeError> {
125        let bincode_config = bincode::config::standard();
126        let mut hasher = Ripemd160::new();
127        hasher.update(service_id.inner());
128        hasher.update(workflow_id.as_bytes());
129        match salt {
130            EventIdSalt::WasmResponse(bytes) => hasher.update(bytes),
131            EventIdSalt::Trigger(trigger_data) => match trigger_data {
132                // For ATProto events, we exclude sequence and timestamp as they are stream-local:
133                // - sequence: Stream-specific sequence number that varies per relay connection
134                // - timestamp: Processing timestamp when the event was received by the stream
135                //
136                // Including only the canonical event data ensures the same event produces
137                // the same EventId hash regardless of which node processes it.
138                // See: https://docs.bsky.app/docs/advanced-guides/timestamps
139                // See: https://docs.bsky.app/blog/relay-ops#relay-upgrade
140                TriggerData::AtProtoEvent {
141                    repo,
142                    collection,
143                    rkey,
144                    action,
145                    cid,
146                    record,
147                    rev,
148                    op_index,
149                    ..
150                } => {
151                    let encoded = bincode::serde::encode_to_vec(
152                        (repo, collection, rkey, action, cid, record, rev, op_index),
153                        bincode_config,
154                    )?;
155                    hasher.update(encoded);
156                }
157                _ => hasher.update(bincode::serde::encode_to_vec(trigger_data, bincode_config)?),
158            },
159        }
160        let result = hasher.finalize();
161        Ok(Self(result.into()))
162    }
163
164    pub fn as_bytes(&self) -> &[u8; 20] {
165        &self.0
166    }
167}
168
169pub enum EventIdSalt<'a> {
170    /// Raw data provided from the component
171    WasmResponse(&'a [u8]),
172    /// Data derived from the trigger
173    Trigger(&'a TriggerData),
174}
175
176impl From<[u8; 20]> for EventId {
177    fn from(value: [u8; 20]) -> Self {
178        Self(value)
179    }
180}
181
182impl From<FixedBytes<20>> for EventId {
183    fn from(value: FixedBytes<20>) -> Self {
184        Self(value.0)
185    }
186}
187
188impl From<EventId> for FixedBytes<20> {
189    fn from(value: EventId) -> Self {
190        FixedBytes(value.0)
191    }
192}
193
194impl std::fmt::Display for EventId {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        write!(f, "{}", const_hex::encode(self.0))
197    }
198}
199
200impl AsRef<[u8]> for EventId {
201    fn as_ref(&self) -> &[u8] {
202        &self.0
203    }
204}
205
206#[derive(Serialize, Deserialize, Clone)]
207#[serde(transparent)]
208pub struct EventOrder([u8; 12]);
209
210impl EventOrder {
211    pub fn new_u64(value: u64) -> Self {
212        let mut bytes = [0; 12];
213        bytes[0..8].copy_from_slice(&value.to_be_bytes());
214        Self(bytes)
215    }
216}
217
218impl From<FixedBytes<12>> for EventOrder {
219    fn from(value: FixedBytes<12>) -> Self {
220        Self(value.0)
221    }
222}
223
224impl From<EventOrder> for FixedBytes<12> {
225    fn from(value: EventOrder) -> Self {
226        FixedBytes(value.0)
227    }
228}
229
230impl std::fmt::Display for EventOrder {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        write!(f, "{}", const_hex::encode(self.0))
233    }
234}
235
236impl AsRef<[u8]> for EventOrder {
237    fn as_ref(&self) -> &[u8] {
238        &self.0
239    }
240}
241
242#[derive(Debug, Error)]
243pub enum SigningError {
244    #[error("Unable to recover signer address: {0:?}")]
245    RecoverSignerAddress(SignatureError),
246
247    #[error("Unable to get data hash: {0:?}")]
248    DataHash(anyhow::Error),
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::AtProtoAction;
255
256    #[test]
257    fn atproto_event_id_ignores_seq_and_timestamp() {
258        let service_id = ServiceId::hash(b"service");
259        let workflow_id = WorkflowId::new("flow1").unwrap();
260
261        let base = TriggerData::AtProtoEvent {
262            sequence: 1,
263            timestamp: 1000,
264            repo: "did:example:123".to_string(),
265            collection: "app.bsky.feed.post".to_string(),
266            rkey: "abc".to_string(),
267            action: AtProtoAction::Create,
268            cid: Some("cid123".to_string()),
269            record: None,
270            rev: Some("rev1".to_string()),
271            op_index: Some(0),
272        };
273
274        let different = match &base {
275            TriggerData::AtProtoEvent {
276                repo,
277                collection,
278                rkey,
279                action,
280                cid,
281                record,
282                rev,
283                op_index,
284                ..
285            } => TriggerData::AtProtoEvent {
286                sequence: 9999,
287                timestamp: 999999,
288                repo: repo.clone(),
289                collection: collection.clone(),
290                rkey: rkey.clone(),
291                action: action.clone(),
292                cid: cid.clone(),
293                record: record.clone(),
294                rev: rev.clone(),
295                op_index: *op_index,
296            },
297            _ => unreachable!(),
298        };
299
300        let id1 = EventId::new(&service_id, &workflow_id, EventIdSalt::Trigger(&base)).unwrap();
301        let id2 =
302            EventId::new(&service_id, &workflow_id, EventIdSalt::Trigger(&different)).unwrap();
303
304        assert_eq!(id1, id2);
305    }
306}