oasis_core_runtime/transaction/
tree.rs

1//! Transaction I/O tree.
2use anyhow::{anyhow, Result};
3
4use super::tags::Tags;
5use crate::{
6    common::{crypto::hash::Hash, key_format::KeyFormat},
7    storage::mkvs::{self, sync::ReadSync, Root, WriteLog},
8};
9
10// NOTE: This should be kept in sync with go/runtime/transaction/transaction.go.
11
12#[derive(Debug)]
13#[repr(u8)]
14enum ArtifactKind {
15    Input = 1,
16    Output = 2,
17}
18
19// Workaround because rust doesn't support `as u8` inside match arms.
20// See https://github.com/rust-lang/rust/issues/44266
21const ARTIFACT_KIND_INPUT: u8 = ArtifactKind::Input as u8;
22const ARTIFACT_KIND_OUTPUT: u8 = ArtifactKind::Output as u8;
23
24/// Key format used for transaction artifacts.
25#[derive(Debug)]
26struct TxnKeyFormat {
27    /// Transaction hash.
28    tx_hash: Hash,
29    /// Artifact kind.
30    kind: ArtifactKind,
31}
32
33impl KeyFormat for TxnKeyFormat {
34    fn prefix() -> u8 {
35        b'T'
36    }
37
38    fn size() -> usize {
39        32 + 1
40    }
41
42    fn encode_atoms(self, atoms: &mut Vec<Vec<u8>>) {
43        atoms.push(self.tx_hash.as_ref().to_vec());
44        match self.kind {
45            ArtifactKind::Input => atoms.push(vec![ARTIFACT_KIND_INPUT]),
46            ArtifactKind::Output => atoms.push(vec![ARTIFACT_KIND_OUTPUT]),
47        }
48    }
49
50    fn decode_atoms(data: &[u8]) -> Self {
51        Self {
52            tx_hash: data[..32].into(),
53            kind: match data[32] {
54                ARTIFACT_KIND_INPUT => ArtifactKind::Input,
55                ARTIFACT_KIND_OUTPUT => ArtifactKind::Output,
56                other => panic!("transaction: malformed artifact kind ({:?})", other),
57            },
58        }
59    }
60}
61
62/// Key format used for emitted tags.
63///
64/// This is kept separate so that clients can query only tags they are
65/// interested in instead of needing to go through all transactions.
66#[derive(Debug, Default)]
67struct TagKeyFormat {
68    /// Tag key.
69    key: Vec<u8>,
70    /// Transaction hash of the transaction that emitted the tag.
71    tx_hash: Hash,
72}
73
74/// Hash used for block emitted tags not tied to a specific transaction.
75pub const TAG_BLOCK_TX_HASH: Hash = Hash([0u8; 32]);
76
77impl KeyFormat for TagKeyFormat {
78    fn prefix() -> u8 {
79        b'E'
80    }
81
82    fn size() -> usize {
83        32
84    }
85
86    fn encode_atoms(self, atoms: &mut Vec<Vec<u8>>) {
87        atoms.push(self.key);
88        atoms.push(self.tx_hash.as_ref().to_vec());
89    }
90
91    fn decode_atoms(data: &[u8]) -> Self {
92        let offset = data.len() - Self::size();
93        let key = data[0..offset].to_vec();
94        let tx_hash = data[offset..].into();
95
96        Self { key, tx_hash }
97    }
98}
99
100/// The input transaction artifacts.
101///
102/// These are the artifacts that are stored CBOR-serialized in the Merkle tree.
103#[derive(Clone, Debug, Default, PartialEq, cbor::Encode, cbor::Decode)]
104#[cbor(as_array)]
105struct InputArtifacts {
106    /// Transaction input.
107    pub input: Vec<u8>,
108    /// Transaction order within the batch.
109    ///
110    /// This is only relevant within the committee that is processing the batch
111    /// and should be ignored once transactions from multiple committees are
112    /// merged together.
113    pub batch_order: u32,
114}
115
116/// The output transaction artifacts.
117///
118/// These are the artifacts that are stored CBOR-serialized in the Merkle tree.
119#[derive(Clone, Debug, Default, PartialEq, cbor::Encode, cbor::Decode)]
120#[cbor(as_array)]
121struct OutputArtifacts {
122    /// Transaction output.
123    pub output: Vec<u8>,
124}
125
126/// A Merkle tree containing transaction artifacts.
127pub struct Tree {
128    io_root: Root,
129    tree: mkvs::OverlayTree<mkvs::Tree>,
130}
131
132impl Tree {
133    /// Create a new transaction artifacts tree.
134    pub fn new(read_syncer: Box<dyn ReadSync>, io_root: Root) -> Self {
135        Self {
136            io_root,
137            tree: mkvs::OverlayTree::new(
138                mkvs::Tree::builder().with_root(io_root).build(read_syncer),
139            ),
140        }
141    }
142
143    /// Add an input transaction artifact.
144    pub fn add_input(&mut self, input: Vec<u8>, batch_order: u32) -> Result<()> {
145        if input.is_empty() {
146            return Err(anyhow!("transaction: no input given"));
147        }
148
149        let tx_hash = Hash::digest_bytes(&input);
150
151        self.tree.insert(
152            &TxnKeyFormat {
153                tx_hash,
154                kind: ArtifactKind::Input,
155            }
156            .encode(),
157            &cbor::to_vec(InputArtifacts { input, batch_order }),
158        )?;
159
160        Ok(())
161    }
162
163    /// Add an output transaction artifact.
164    pub fn add_output(&mut self, tx_hash: Hash, output: Vec<u8>, tags: Tags) -> Result<()> {
165        self.tree.insert(
166            &TxnKeyFormat {
167                tx_hash,
168                kind: ArtifactKind::Output,
169            }
170            .encode(),
171            &cbor::to_vec(OutputArtifacts { output }),
172        )?;
173
174        // Add tags if specified.
175        for tag in tags {
176            self.tree.insert(
177                &TagKeyFormat {
178                    key: tag.key,
179                    tx_hash,
180                }
181                .encode(),
182                &tag.value,
183            )?;
184        }
185
186        Ok(())
187    }
188
189    /// Add block tags.
190    pub fn add_block_tags(&mut self, tags: Tags) -> Result<()> {
191        for tag in tags {
192            self.tree.insert(
193                &TagKeyFormat {
194                    key: tag.key,
195                    tx_hash: TAG_BLOCK_TX_HASH,
196                }
197                .encode(),
198                &tag.value,
199            )?;
200        }
201
202        Ok(())
203    }
204
205    /// Commit updates to the underlying Merkle tree and return the write
206    /// log and root hash.
207    pub fn commit(&mut self) -> Result<(WriteLog, Hash)> {
208        self.tree
209            .commit_both(self.io_root.namespace, self.io_root.version)
210    }
211
212    /// Fetch the input artifact for the given transaction hash.
213    pub fn get_input(&self, tx_hash: Hash) -> Result<Option<Vec<u8>>> {
214        let raw = self.tree.get(
215            &TxnKeyFormat {
216                tx_hash,
217                kind: ArtifactKind::Input,
218            }
219            .encode(),
220        )?;
221        match raw {
222            Some(raw) => {
223                let ia: InputArtifacts = cbor::from_slice(&raw)?;
224                Ok(Some(ia.input))
225            }
226            None => Ok(None),
227        }
228    }
229
230    /// Fetch the output artifact for the given transaction hash.
231    pub fn get_output(&self, tx_hash: Hash) -> Result<Option<Vec<u8>>> {
232        let raw = self.tree.get(
233            &TxnKeyFormat {
234                tx_hash,
235                kind: ArtifactKind::Output,
236            }
237            .encode(),
238        )?;
239        match raw {
240            Some(raw) => {
241                let oa: OutputArtifacts = cbor::from_slice(&raw)?;
242                Ok(Some(oa.output))
243            }
244            None => Ok(None),
245        }
246    }
247}
248
249#[cfg(test)]
250mod test {
251    use crate::storage::mkvs::sync::*;
252
253    use super::{super::tags::Tag, *};
254
255    #[test]
256    fn test_transaction() {
257        let mut tree = Tree::new(
258            Box::new(NoopReadSyncer),
259            Root {
260                hash: Hash::empty_hash(),
261                ..Default::default()
262            },
263        );
264
265        let input = b"this goes in".to_vec();
266        let tx_hash = Hash::digest_bytes(&input);
267        let orig_tx_hash = tx_hash;
268        tree.add_input(input, 0).unwrap();
269        tree.add_output(
270            tx_hash,
271            b"and this comes out".to_vec(),
272            vec![Tag::new(b"tag1".to_vec(), b"value1".to_vec())],
273        )
274        .unwrap();
275
276        for i in 0..20 {
277            let input = format!("this goes in ({})", i).into_bytes();
278            let tx_hash = Hash::digest_bytes(&input);
279
280            tree.add_input(input, i + 1).unwrap();
281            tree.add_output(
282                tx_hash,
283                b"and this comes out".to_vec(),
284                vec![
285                    Tag::new(b"tagA".to_vec(), b"valueA".to_vec()),
286                    Tag::new(b"tagB".to_vec(), b"valueB".to_vec()),
287                ],
288            )
289            .unwrap();
290        }
291
292        // NOTE: This root is synced with go/runtime/transaction/transaction_test.go.
293        let (_, root_hash) = tree.commit().unwrap();
294        assert_eq!(
295            format!("{:?}", root_hash),
296            "8399ffa753987b00ec6ab251337c6b88e40812662ed345468fcbf1dbdd16321c",
297        );
298
299        // Accessors.
300        let dec_input = tree.get_input(orig_tx_hash).unwrap();
301        assert_eq!(dec_input, Some(b"this goes in".to_vec()));
302        let dec_output = tree.get_output(orig_tx_hash).unwrap();
303        assert_eq!(dec_output, Some(b"and this comes out".to_vec()));
304
305        let dec_input = tree.get_input(Hash::empty_hash()).unwrap();
306        assert!(dec_input.is_none());
307        let dec_output = tree.get_output(Hash::empty_hash()).unwrap();
308        assert!(dec_output.is_none());
309    }
310}