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
213#[cfg(test)]
214mod test {
215    use crate::storage::mkvs::sync::*;
216
217    use super::{super::tags::Tag, *};
218
219    #[test]
220    fn test_transaction() {
221        let mut tree = Tree::new(
222            Box::new(NoopReadSyncer),
223            Root {
224                hash: Hash::empty_hash(),
225                ..Default::default()
226            },
227        );
228
229        let input = b"this goes in".to_vec();
230        let tx_hash = Hash::digest_bytes(&input);
231        tree.add_input(input, 0).unwrap();
232        tree.add_output(
233            tx_hash,
234            b"and this comes out".to_vec(),
235            vec![Tag::new(b"tag1".to_vec(), b"value1".to_vec())],
236        )
237        .unwrap();
238
239        for i in 0..20 {
240            let input = format!("this goes in ({})", i).into_bytes();
241            let tx_hash = Hash::digest_bytes(&input);
242
243            tree.add_input(input, i + 1).unwrap();
244            tree.add_output(
245                tx_hash,
246                b"and this comes out".to_vec(),
247                vec![
248                    Tag::new(b"tagA".to_vec(), b"valueA".to_vec()),
249                    Tag::new(b"tagB".to_vec(), b"valueB".to_vec()),
250                ],
251            )
252            .unwrap();
253        }
254
255        // NOTE: This root is synced with go/runtime/transaction/transaction_test.go.
256        let (_, root_hash) = tree.commit().unwrap();
257        assert_eq!(
258            format!("{:?}", root_hash),
259            "8399ffa753987b00ec6ab251337c6b88e40812662ed345468fcbf1dbdd16321c",
260        );
261    }
262}