indexer: improved rollback; global: snapshot

This commit is contained in:
nym21
2025-02-18 22:43:12 +01:00
parent a122333aaa
commit 15f2e05192
29 changed files with 708 additions and 523 deletions
+37 -6
View File
@@ -1,4 +1,8 @@
use std::{collections::BTreeMap, error, mem, path::Path};
use std::{
collections::{BTreeMap, BTreeSet},
error, mem,
path::Path,
};
use fjall::{
PartitionCreateOptions, PersistMode, ReadTransaction, Result, Slice, TransactionalKeyspace,
@@ -17,8 +21,11 @@ pub struct Store<Key, Value> {
part: TransactionalPartitionHandle,
rtx: ReadTransaction,
puts: BTreeMap<Key, Value>,
dels: BTreeSet<Key>,
}
const CHECK_COLLISISONS: bool = true;
impl<K, V> Store<K, V>
where
K: Into<Slice> + Ord + Immutable + IntoBytes,
@@ -48,6 +55,7 @@ where
part,
rtx,
puts: BTreeMap::new(),
dels: BTreeSet::new(),
})
}
@@ -63,21 +71,44 @@ where
pub fn insert_if_needed(&mut self, key: K, value: V, height: Height) {
if self.needs(height) {
if !self.dels.is_empty() {
unreachable!("Shouldn't reach this");
// self.dels.remove(&key);
}
self.puts.insert(key, value);
}
}
pub fn remove(&mut self, key: K) {
if !self.puts.is_empty() {
unreachable!("Shouldn't reach this");
// self.puts.remove(&key);
}
self.dels.insert(key);
}
pub fn commit(&mut self, height: Height) -> Result<()> {
if self.has(height) && self.puts.is_empty() {
if self.has(height) && self.puts.is_empty() && self.dels.is_empty() {
return Ok(());
}
self.meta.export(self.len(), height)?;
let mut wtx = self.keyspace.write_tx();
mem::take(&mut self.puts)
mem::take(&mut self.dels)
.into_iter()
.for_each(|(key, value)| wtx.insert(&self.part, key, value));
.for_each(|key| wtx.remove(&self.part, key));
mem::take(&mut self.puts).into_iter().for_each(|(key, value)| {
if CHECK_COLLISISONS {
if let Ok(Some(value)) = wtx.get(&self.part, key.as_bytes()) {
dbg!(value);
unreachable!();
}
}
wtx.insert(&self.part, key, value)
});
wtx.commit()?;
@@ -88,12 +119,12 @@ where
Ok(())
}
pub fn height(&self) -> Option<&Height> {
pub fn height(&self) -> Option<Height> {
self.meta.height()
}
pub fn len(&self) -> usize {
self.meta.len() + self.puts.len()
self.meta.len() + self.puts.len() - self.dels.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
+2 -2
View File
@@ -67,8 +67,8 @@ impl StoreMeta {
path.join("version")
}
pub fn height(&self) -> Option<&Height> {
self.height.as_ref()
pub fn height(&self) -> Option<Height> {
self.height
}
pub fn needs(&self, height: Height) -> bool {
self.height.is_none_or(|self_height| height > self_height)
+117 -132
View File
@@ -1,16 +1,19 @@
use std::{path::Path, thread};
use storable_vec::Version;
use storable_vec::{Value, Version, CACHED_GETS};
use crate::{AddressHash, Addressindex, BlockHashPrefix, Height, TxidPrefix, Txindex};
use crate::{
AddressHash, Addressbytes, Addressindex, Addresstype, BlockHashPrefix, Height, Indexes, TxidPrefix, Txindex,
};
mod base;
mod meta;
// mod version;
pub use base::*;
pub use meta::*;
use super::StorableVecs;
pub struct Fjalls {
pub addresshash_to_addressindex: Store<AddressHash, Addressindex>,
pub blockhash_prefix_to_height: Store<BlockHashPrefix, Height>,
@@ -30,135 +33,135 @@ impl Fjalls {
})
}
// pub fn rollback_from(
// &mut self,
// _wtx: &mut WriteTransaction,
// _height: Height,
// _exit: &Exit,
// ) -> color_eyre::Result<()> {
// panic!();
// let mut txindex = None;
pub fn rollback(&mut self, vecs: &StorableVecs<CACHED_GETS>, starting_indexes: &Indexes) -> color_eyre::Result<()> {
vecs.height_to_blockhash
.iter_from(starting_indexes.height, |(_, blockhash)| {
let blockhash = blockhash.as_ref();
let blockhash_prefix = BlockHashPrefix::from(blockhash);
self.blockhash_prefix_to_height.remove(blockhash_prefix);
Ok(())
})?;
// wtx.range(self.height_to_blockhash.data(), Slice::from(height)..)
// .try_for_each(|slice| -> color_eyre::Result<()> {
// let (height_slice, slice_blockhash) = slice?;
// let blockhash = BlockHash::from_slice(&slice_blockhash)?;
vecs.txindex_to_txid.iter_from(starting_indexes.txindex, |(_, txid)| {
let txid = txid.as_ref();
let txid_prefix = TxidPrefix::from(txid);
self.txid_prefix_to_txindex.remove(txid_prefix);
Ok(())
})?;
// wtx.remove(self.height_to_blockhash.data(), height_slice);
vecs.height_to_first_p2pk65index
.iter_from(starting_indexes.height, |(_, index)| {
if let Some(typedbytes) = vecs
.p2pk65index_to_p2pk65addressbytes
.get(index.into_inner())?
.map(Value::into_inner)
{
let bytes = Addressbytes::from(typedbytes);
let hash = AddressHash::from((&bytes, Addresstype::P2PK65));
self.addresshash_to_addressindex.remove(hash);
}
Ok(())
})?;
// wtx.remove(self.blockhash_prefix_to_height.data(), blockhash.prefix());
vecs.height_to_first_p2pk33index
.iter_from(starting_indexes.height, |(_, index)| {
if let Some(typedbytes) = vecs
.p2pk33index_to_p2pk33addressbytes
.get(index.into_inner())?
.map(Value::into_inner)
{
let bytes = Addressbytes::from(typedbytes);
let hash = AddressHash::from((&bytes, Addresstype::P2PK33));
self.addresshash_to_addressindex.remove(hash);
}
Ok(())
})?;
// if txindex.is_none() {
// txindex.replace(
// wtx.get(self.height_to_first_txindex.data(), height_slice)?
// .context("for height to have first txindex")?,
// );
// }
// wtx.remove(self.height_to_first_txindex.data(), height_slice);
// wtx.remove(self.height_to_last_txindex.data(), height_slice);
vecs.height_to_first_p2pkhindex
.iter_from(starting_indexes.height, |(_, index)| {
if let Some(typedbytes) = vecs
.p2pkhindex_to_p2pkhaddressbytes
.get(index.into_inner())?
.map(Value::into_inner)
{
let bytes = Addressbytes::from(typedbytes);
let hash = AddressHash::from((&bytes, Addresstype::P2PKH));
self.addresshash_to_addressindex.remove(hash);
}
Ok(())
})?;
// Ok(())
// })?;
vecs.height_to_first_p2shindex
.iter_from(starting_indexes.height, |(_, index)| {
if let Some(typedbytes) = vecs
.p2shindex_to_p2shaddressbytes
.get(index.into_inner())?
.map(Value::into_inner)
{
let bytes = Addressbytes::from(typedbytes);
let hash = AddressHash::from((&bytes, Addresstype::P2SH));
self.addresshash_to_addressindex.remove(hash);
}
Ok(())
})?;
// let txindex = txindex.context("txindex to not be none by now")?;
vecs.height_to_first_p2trindex
.iter_from(starting_indexes.height, |(_, index)| {
if let Some(typedbytes) = vecs
.p2trindex_to_p2traddressbytes
.get(index.into_inner())?
.map(Value::into_inner)
{
let bytes = Addressbytes::from(typedbytes);
let hash = AddressHash::from((&bytes, Addresstype::P2TR));
self.addresshash_to_addressindex.remove(hash);
}
Ok(())
})?;
// wtx.range(self.txindex_to_txid.data(), Slice::from(txindex)..)
// .try_for_each(|slice| -> color_eyre::Result<()> {
// let (slice_txindex, slice_txid) = slice?;
// let txindex = Txindex::from(slice_txindex);
// let txid = Txid::from_slice(&slice_txid)?;
vecs.height_to_first_p2wpkhindex
.iter_from(starting_indexes.height, |(_, index)| {
if let Some(typedbytes) = vecs
.p2wpkhindex_to_p2wpkhaddressbytes
.get(index.into_inner())?
.map(Value::into_inner)
{
let bytes = Addressbytes::from(typedbytes);
let hash = AddressHash::from((&bytes, Addresstype::P2WPKH));
self.addresshash_to_addressindex.remove(hash);
}
Ok(())
})?;
// wtx.remove(self.txindex_to_txid.data(), Slice::from(txindex));
// wtx.remove(self.txindex_to_height.data(), Slice::from(txindex));
// wtx.remove(self.txid_prefix_to_txindex.data(), txid.prefix());
vecs.height_to_first_p2wshindex
.iter_from(starting_indexes.height, |(_, index)| {
if let Some(typedbytes) = vecs
.p2wshindex_to_p2wshaddressbytes
.get(index.into_inner())?
.map(Value::into_inner)
{
let bytes = Addressbytes::from(typedbytes);
let hash = AddressHash::from((&bytes, Addresstype::P2WSH));
self.addresshash_to_addressindex.remove(hash);
}
Ok(())
})?;
// Ok(())
// })?;
self.commit(starting_indexes.height.decremented())?;
// let txoutindex = Txoutindex::from(txindex);
Ok(())
}
// let mut addressindexes = BTreeSet::new();
// wtx.range(self.txoutindex_to_amount.data(), Slice::from(txoutindex)..)
// .try_for_each(|slice| -> color_eyre::Result<()> {
// let (txoutindex_slice, _) = slice?;
// wtx.remove(self.txoutindex_to_amount.data(), txoutindex_slice);
// if let Some(addressindex_slice) =
// wtx.get(self.txoutindex_to_addressindex.data(), txoutindex_slice)?
// {
// wtx.remove(self.txoutindex_to_addressindex.data(), txoutindex_slice);
// let addressindex = Addressindex::from(addressindex_slice);
// addressindexes.insert(addressindex);
// let txoutindex = Txoutindex::from(txoutindex_slice);
// let addresstxoutindex = Addresstxoutindex::from((addressindex, txoutindex));
// wtx.remove(
// self.addressindex_to_txoutindexes.data(),
// Slice::from(addresstxoutindex),
// );
// }
// Ok(())
// })?;
// addressindexes
// .into_iter()
// .filter(|addressindex| {
// let is_empty = wtx
// .prefix(
// self.addressindex_to_txoutindexes.data(),
// Slice::from(*addressindex),
// )
// .next()
// .is_none();
// is_empty
// })
// .try_for_each(|addressindex| -> color_eyre::Result<()> {
// let addressindex_slice = Slice::from(addressindex);
// let addressbytes = Addressbytes::from(
// wtx.get(
// self.addressindex_to_addressbytes.data(),
// &addressindex_slice,
// )?
// .context("addressindex_to_address to have value")?,
// );
// wtx.remove(
// self.addressbytes_prefix_to_addressindex.data(),
// addressbytes.prefix(),
// );
// wtx.remove(
// self.addressindex_to_addressbytes.data(),
// &addressindex_slice,
// );
// wtx.remove(self.addressindex_to_addresstype.data(), &addressindex_slice);
// Ok(())
// })?;
//
// todo!("clear addresstxoutindexes_out")
// todo!("clear addresstxoutindexes_in")
// todo!("clear zero_txoutindexes")
// todo!("clear txindexvout_to_txoutindex")
// Ok(())
// }
pub fn min_height(&self) -> Option<Height> {
pub fn starting_height(&self) -> Height {
[
self.addresshash_to_addressindex.height(),
self.blockhash_prefix_to_height.height(),
self.txid_prefix_to_txindex.height(),
]
.into_iter()
.map(|height| height.map(Height::incremented).unwrap_or_default())
.min()
.flatten()
.cloned()
.unwrap()
}
pub fn commit(&mut self, height: Height) -> fjall::Result<()> {
@@ -176,22 +179,4 @@ impl Fjalls {
Ok(())
})
}
// pub fn udpate_meta(&self, wtx: &mut WriteTransaction, height: Height) {
// self.addressbytes_prefix_to_addressindex.update_meta(wtx, height);
// self.blockhash_prefix_to_height.update_meta(wtx, height);
// self.txid_prefix_to_txindex.update_meta(wtx, height);
// }
// pub fn export(self, height: Height) -> Result<(), snkrj::Error> {
// thread::scope(|scope| {
// vec![
// scope.spawn(|| self.addressbytes_prefix_to_addressindex.export(height)),
// scope.spawn(|| self.blockhash_prefix_to_height.export(height)),
// scope.spawn(|| self.txid_prefix_to_txindex.export(height)),
// ]
// .into_iter()
// .try_for_each(|handle| -> Result<(), snkrj::Error> { handle.join().unwrap() })
// })
// }
}
+4 -4
View File
@@ -28,14 +28,14 @@ where
}
pub fn flush(&mut self, height: Height) -> io::Result<()> {
if self.needs(height) {
height.write(&self.path_height())?;
}
height.write(&self.path_height())?;
self.vec.flush()
}
pub fn truncate_if_needed(&mut self, index: I, height: Height) -> storable_vec::Result<Option<T>> {
height.write(&self.path_height())?;
if self.height.is_none_or(|self_height| self_height != height) {
height.write(&self.path_height())?;
}
self.vec.truncate_if_needed(index)
}
+101 -109
View File
@@ -1,14 +1,17 @@
use std::{collections::BTreeMap, fs, io, path::Path};
use std::{fs, io, path::Path};
use exit::Exit;
use rayon::prelude::*;
use storable_vec::{AnyJsonStorableVec, Version, CACHED_GETS};
use crate::structs::{
Addressbytes, Addressindex, Addresstype, Addresstypeindex, BlockHash, Emptyindex, Height, LockTime, Multisigindex,
Opreturnindex, P2PK33AddressBytes, P2PK33index, P2PK65AddressBytes, P2PK65index, P2PKHAddressBytes, P2PKHindex,
P2SHAddressBytes, P2SHindex, P2TRAddressBytes, P2TRindex, P2WPKHAddressBytes, P2WPKHindex, P2WSHAddressBytes,
P2WSHindex, Pushonlyindex, Sats, Timestamp, TxVersion, Txid, Txindex, Txinindex, Txoutindex, Unknownindex, Weight,
use crate::{
structs::{
Addressbytes, Addressindex, Addresstype, Addresstypeindex, BlockHash, Emptyindex, Height, LockTime,
Multisigindex, Opreturnindex, P2PK33AddressBytes, P2PK33index, P2PK65AddressBytes, P2PK65index,
P2PKHAddressBytes, P2PKHindex, P2SHAddressBytes, P2SHindex, P2TRAddressBytes, P2TRindex, P2WPKHAddressBytes,
P2WPKHindex, P2WSHAddressBytes, P2WSHindex, Pushonlyindex, Sats, Timestamp, TxVersion, Txid, Txindex,
Txinindex, Txoutindex, Unknownindex, Weight,
},
Indexes,
};
mod base;
@@ -58,8 +61,6 @@ pub struct StorableVecs<const MODE: u8> {
pub txoutindex_to_value: StorableVec<Txoutindex, Sats, MODE>,
}
// const UNSAFE_BLOCKS: usize = 1000;
impl<const MODE: u8> StorableVecs<MODE> {
pub fn import(path: &Path) -> color_eyre::Result<Self> {
fs::create_dir_all(path)?;
@@ -180,113 +181,104 @@ impl<const MODE: u8> StorableVecs<MODE> {
})
}
#[allow(unused)]
pub fn rollback_from(&mut self, height: Height, exit: &Exit) -> storable_vec::Result<()> {
let prev_height = height.decremented();
pub fn rollback(&mut self, starting_indexes: &Indexes) -> storable_vec::Result<()> {
let saved_height = starting_indexes.height.decremented();
let mut truncated_indexes: BTreeMap<String, Option<usize>> = BTreeMap::new();
// We don't want to override the starting indexes so we cut from n + 1
let height = starting_indexes.height.incremented();
let addressindex = self
.height_to_first_addressindex
.truncate_if_needed(height, prev_height)?;
let txindex = self.height_to_first_txindex.truncate_if_needed(height, prev_height)?;
let txinindex = self.height_to_first_txinindex.truncate_if_needed(height, prev_height)?;
let txoutindex = self
.height_to_first_txoutindex
.truncate_if_needed(height, prev_height)?;
let p2pk33index = self
.height_to_first_p2pk33index
.truncate_if_needed(height, prev_height)?;
let p2pk65index = self
.height_to_first_p2pk65index
.truncate_if_needed(height, prev_height)?;
let p2pkhindex = self
.height_to_first_p2pkhindex
.truncate_if_needed(height, prev_height)?;
let p2shindex = self.height_to_first_p2shindex.truncate_if_needed(height, prev_height)?;
let p2trindex = self.height_to_first_p2trindex.truncate_if_needed(height, prev_height)?;
let p2wpkhindex = self
.height_to_first_p2wpkhindex
.truncate_if_needed(height, prev_height)?;
let p2wshindex = self
.height_to_first_p2wshindex
.truncate_if_needed(height, prev_height)?;
self.height_to_blockhash.truncate_if_needed(height, prev_height)?;
self.height_to_difficulty.truncate_if_needed(height, prev_height)?;
self.height_to_first_addressindex
.truncate_if_needed(height, saved_height)?;
self.height_to_first_emptyindex
.truncate_if_needed(height, prev_height)?;
.truncate_if_needed(height, saved_height)?;
self.height_to_first_multisigindex
.truncate_if_needed(height, prev_height)?;
.truncate_if_needed(height, saved_height)?;
self.height_to_first_opreturnindex
.truncate_if_needed(height, prev_height)?;
.truncate_if_needed(height, saved_height)?;
self.height_to_first_p2pk33index
.truncate_if_needed(height, saved_height)?;
self.height_to_first_p2pk65index
.truncate_if_needed(height, saved_height)?;
self.height_to_first_p2pkhindex
.truncate_if_needed(height, saved_height)?;
self.height_to_first_p2shindex
.truncate_if_needed(height, saved_height)?;
self.height_to_first_p2trindex
.truncate_if_needed(height, saved_height)?;
self.height_to_first_p2wpkhindex
.truncate_if_needed(height, saved_height)?;
self.height_to_first_p2wshindex
.truncate_if_needed(height, saved_height)?;
self.height_to_first_pushonlyindex
.truncate_if_needed(height, prev_height)?;
.truncate_if_needed(height, saved_height)?;
self.height_to_first_txindex.truncate_if_needed(height, saved_height)?;
self.height_to_first_txinindex
.truncate_if_needed(height, saved_height)?;
self.height_to_first_txoutindex
.truncate_if_needed(height, saved_height)?;
self.height_to_first_unknownindex
.truncate_if_needed(height, prev_height)?;
self.height_to_size.truncate_if_needed(height, prev_height)?;
self.height_to_timestamp.truncate_if_needed(height, prev_height)?;
self.height_to_weight.truncate_if_needed(height, prev_height)?;
.truncate_if_needed(height, saved_height)?;
if let Some(addressindex) = addressindex {
self.addressindex_to_addresstype
.truncate_if_needed(addressindex, prev_height)?;
self.addressindex_to_addresstypeindex
.truncate_if_needed(addressindex, prev_height)?;
self.addressindex_to_height
.truncate_if_needed(addressindex, prev_height)?;
}
// Now we can cut everything that's out of date
let &Indexes {
addressindex,
height,
p2pk33index,
p2pk65index,
p2pkhindex,
p2shindex,
p2trindex,
p2wpkhindex,
p2wshindex,
txindex,
txinindex,
txoutindex,
..
} = starting_indexes;
if let Some(p2pk33index) = p2pk33index {
self.p2pk33index_to_p2pk33addressbytes
.truncate_if_needed(p2pk33index, prev_height)?;
}
if let Some(p2pk65index) = p2pk65index {
self.p2pk65index_to_p2pk65addressbytes
.truncate_if_needed(p2pk65index, prev_height)?;
}
if let Some(p2pkhindex) = p2pkhindex {
self.p2pkhindex_to_p2pkhaddressbytes
.truncate_if_needed(p2pkhindex, prev_height)?;
}
if let Some(p2shindex) = p2shindex {
self.p2shindex_to_p2shaddressbytes
.truncate_if_needed(p2shindex, prev_height)?;
}
if let Some(p2trindex) = p2trindex {
self.p2trindex_to_p2traddressbytes
.truncate_if_needed(p2trindex, prev_height)?;
}
if let Some(p2wpkhindex) = p2wpkhindex {
self.p2wpkhindex_to_p2wpkhaddressbytes
.truncate_if_needed(p2wpkhindex, prev_height)?;
}
if let Some(p2wshindex) = p2wshindex {
self.p2wshindex_to_p2wshaddressbytes
.truncate_if_needed(p2wshindex, prev_height);
}
self.height_to_blockhash.truncate_if_needed(height, saved_height)?;
self.height_to_difficulty.truncate_if_needed(height, saved_height)?;
self.height_to_size.truncate_if_needed(height, saved_height)?;
self.height_to_timestamp.truncate_if_needed(height, saved_height)?;
self.height_to_weight.truncate_if_needed(height, saved_height)?;
if let Some(txindex) = txindex {
self.txindex_to_first_txinindex
.truncate_if_needed(txindex, prev_height)?;
self.txindex_to_first_txoutindex
.truncate_if_needed(txindex, prev_height)?;
self.txindex_to_height.truncate_if_needed(txindex, prev_height)?;
self.txindex_to_locktime.truncate_if_needed(txindex, prev_height)?;
self.txindex_to_txid.truncate_if_needed(txindex, prev_height)?;
self.txindex_to_txversion.truncate_if_needed(txindex, prev_height)?;
}
self.addressindex_to_addresstype
.truncate_if_needed(addressindex, saved_height)?;
self.addressindex_to_addresstypeindex
.truncate_if_needed(addressindex, saved_height)?;
self.addressindex_to_height
.truncate_if_needed(addressindex, saved_height)?;
if let Some(txinindex) = txinindex {
self.txinindex_to_txoutindex
.truncate_if_needed(txinindex, prev_height)?;
}
self.p2pk33index_to_p2pk33addressbytes
.truncate_if_needed(p2pk33index, saved_height)?;
self.p2pk65index_to_p2pk65addressbytes
.truncate_if_needed(p2pk65index, saved_height)?;
self.p2pkhindex_to_p2pkhaddressbytes
.truncate_if_needed(p2pkhindex, saved_height)?;
self.p2shindex_to_p2shaddressbytes
.truncate_if_needed(p2shindex, saved_height)?;
self.p2trindex_to_p2traddressbytes
.truncate_if_needed(p2trindex, saved_height)?;
self.p2wpkhindex_to_p2wpkhaddressbytes
.truncate_if_needed(p2wpkhindex, saved_height)?;
self.p2wshindex_to_p2wshaddressbytes
.truncate_if_needed(p2wshindex, saved_height)?;
if let Some(txoutindex) = txoutindex {
self.txoutindex_to_addressindex
.truncate_if_needed(txoutindex, prev_height)?;
self.txoutindex_to_value.truncate_if_needed(txoutindex, prev_height)?;
}
self.txindex_to_first_txinindex
.truncate_if_needed(txindex, saved_height)?;
self.txindex_to_first_txoutindex
.truncate_if_needed(txindex, saved_height)?;
self.txindex_to_height.truncate_if_needed(txindex, saved_height)?;
self.txindex_to_locktime.truncate_if_needed(txindex, saved_height)?;
self.txindex_to_txid.truncate_if_needed(txindex, saved_height)?;
self.txindex_to_txversion.truncate_if_needed(txindex, saved_height)?;
self.txinindex_to_txoutindex
.truncate_if_needed(txinindex, saved_height)?;
self.txoutindex_to_addressindex
.truncate_if_needed(txoutindex, saved_height)?;
self.txoutindex_to_value.truncate_if_needed(txoutindex, saved_height)?;
Ok(())
}
@@ -297,12 +289,12 @@ impl<const MODE: u8> StorableVecs<MODE> {
.try_for_each(|vec| vec.flush(height))
}
pub fn min_height(&mut self) -> color_eyre::Result<Option<Height>> {
Ok(self
.as_mut_any_vec_slice()
pub fn starting_height(&mut self) -> Height {
self.as_mut_any_vec_slice()
.into_iter()
.map(|vec| vec.height().unwrap_or_default())
.min())
.map(|vec| vec.height().map(Height::incremented).unwrap_or_default())
.min()
.unwrap()
}
pub fn as_any_json_vec_slice(&self) -> [&dyn AnyJsonStorableVec; 40] {