rostrum 14.0.1

An efficient implementation of Electrum Server with token support
Documentation
use std::{collections::HashSet, sync::Arc};

use crate::{
    chaindef::ScriptHash,
    encode::compute_outpoint_hash,
    indexes::DBRow,
    query::{
        queryfilter::QueryFilter,
        queryutil::{get_tx_funding_prevout, height_by_txid, tx_funding_outpoint},
        tx::TxQuery,
    },
};
use bitcoin_hashes::{ripemd160, sha256, Hash};
use bitcoincash::{
    blockdata::{
        opcodes::all::{OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160},
        script::Builder,
    },
    Txid,
};
use futures::{stream, StreamExt, TryStreamExt};
use rayon::slice::ParallelSliceMut;

use crate::{
    chaindef::Transaction, indexes::scripthashindex::ScriptHashIndexRow, store::DBStore,
    utilscript::read_push_from_script,
};
use anyhow::*;

#[derive(Debug, Serialize, Deserialize)]
pub struct CRC20 {
    pub symbol: String,
    pub decimals: u8,
    pub name: String,
    pub genesis_out: ScriptHash,
}

const SCRIPT_POSTFIX: &[u8] = &[
    0x53, 0x7a, 0x7c, 0xad, 0x7c, 0x7f, 0x75, 0xa9, 0x03, 0x76, 0xa9, 0x14, 0x7c, 0x7e, 0x02, 0x88,
    0xac, 0x7e, 0x00, 0xcd, 0x87,
];

fn hash160(data: &[u8]) -> [u8; 20] {
    let sha = sha256::Hash::hash(data);
    ripemd160::Hash::hash(&sha).into_inner()
}

pub fn parse_token_crc20(token_genesis: &Transaction) -> Option<CRC20> {
    // contract GenesisOutput(pubkey recipientPK, bytes metadata, int symbolLength) {
    //    function reveal(sig recipientSig) {
    //        require(checkSig(recipientSig, recipientPK));
    //        bytes20 symbolHash = hash160(metadata.split(symbolLength)[0]);
    //        bytes25 outLockingBytecode = new LockingBytecodeP2PKH(symbolHash);
    //        require(tx.outputs[0].lockingBytecode == outLockingBytecode);
    //    }
    // }

    let genesis_input = token_genesis.input.first()?;
    let genesis_output = token_genesis.output.first()?;

    let (iter, signature) =
        read_push_from_script(genesis_input.script_sig.as_bytes().iter()).ok()?;
    let signature = signature?;

    if signature.len() < 65 || signature.len() > 73 {
        // does not look like a signature
        return None;
    }

    let (_iter, contract) = read_push_from_script(iter).ok()?;
    let contract = contract?;

    let (iter, symbollength) = read_push_from_script(contract.iter()).ok()?;
    let symbollength = *symbollength?.first()? as usize;
    let (iter, metadata) = read_push_from_script(iter).ok()?;
    let metadata = metadata?;

    if metadata.len() < symbollength + 1
    /* token + decimals */
    {
        return None;
    }
    let symbol = &metadata[0..symbollength];
    let symbol = String::from_utf8(symbol.to_vec()).ok()?;
    let decimals = *metadata.get(symbollength)?;

    let name_bytes = &metadata[symbollength + 1..];
    let name = if !name_bytes.is_empty() {
        String::from_utf8(name_bytes.to_vec()).ok()?
    } else {
        "".to_string()
    };

    // some final validation
    let (iter, pubkey) = read_push_from_script(iter).ok()?;
    if pubkey?.len() != 65 {
        return None;
    }

    // check that remainder of contract fits template
    let remainder: Vec<u8> = iter.cloned().collect();
    if !remainder.eq(SCRIPT_POSTFIX) {
        return None;
    }

    // validate outpoint
    let symbol_hash = hash160(symbol.as_bytes());
    let expected_scripthash = ScriptHash::from_script(
        &Builder::new()
            .push_opcode(OP_DUP)
            .push_opcode(OP_HASH160)
            .push_slice(&symbol_hash)
            .push_opcode(OP_EQUALVERIFY)
            .push_opcode(OP_CHECKSIG)
            .into_script(),
    );
    if ScriptHash::from_script(&genesis_output.script_pubkey) != expected_scripthash {
        return None;
    }

    Some(CRC20 {
        symbol,
        decimals,
        name,
        genesis_out: expected_scripthash,
    })
}

async fn fetch_reveal_transactions(
    confirmed: &Arc<DBStore>,
    reveal_output_scripthash: &ScriptHash,
) -> Result<Vec<(Txid, u32)>> {
    // We need to validate that this reveil is first.

    // Only look for funding entries (outputs), not spending entries (inputs)
    let (scan_prefix, scan_bitmask) = ScriptHashIndexRow::filter_by_outputs(
        reveal_output_scripthash.into_inner(),
        &QueryFilter::default(),
    );
    let (query, stream) = confirmed
        .scan(ScriptHashIndexRow::CF, scan_prefix, scan_bitmask)
        .await;

    let txs: Vec<(Txid, u32)> = stream
        .then(|row| async move { ScriptHashIndexRow::from_row(&row).outpointhash() })
        .filter_map(|oph| async move { tx_funding_outpoint(confirmed, &oph).await })
        .filter_map(|tx| async move {
            // From spec: If this UTXO is not the first output of the transaction, filter it out.
            match tx.index() {
                0 => Some(tx.txid()),
                _ => None,
            }
        })
        .filter_map(|txid| async move {
            let h = height_by_txid(confirmed, &txid).await;
            h.map(|h| (txid, h))
        })
        .collect()
        .await;
    query.await??;
    Ok(txs)
}

fn is_genesis_tx(tx: &Transaction) -> bool {
    let potential_token_ids: HashSet<[u8; 32]> = tx
        .input
        .iter()
        .map(|i| i.previous_output.txid.into_inner())
        .collect();

    for o in &tx.output {
        match &o.token {
            Some(token) => {
                if potential_token_ids.contains(&token.id.into_inner()) {
                    return true;
                }
            }
            None => continue,
        }
    }
    false
}

pub async fn get_token_crc20(
    token_genesis: &Transaction,
    confirmed: &Arc<DBStore>,
    txquery: &TxQuery,
) -> Result<Option<CRC20>> {
    let crc20 = match parse_token_crc20(token_genesis) {
        Some(c) => c,
        None => return Ok(None),
    };

    // From CRC20 spec at crc20.cash:

    // The Fair Genesis Height of a token is defined as MAX(H_commit, H_reveal-20),
    // where H_commit (H_reveal) is the height of the transaction committing
    // (revealing) the metadata, respectively.
    //
    // Given a symbol, calculate the address of the Symbol UTXO. Then we use
    // blockchain.address.listunspent to find this address's UTXO list, sort
    // them according to the Fair Genesis Height and the genesis transaction's hash ID, and then filter them:
    //
    // If this UTXO has not been packaged into a block, filter it out.
    // If this UTXO is not the first output of the transaction, filter it out.
    // If this UTXO is not an output of the Genesis Transaction, filter it out.
    //    This requires checking whether other outputs have the token category attribute,
    //    and the Symbol UTXO itself does not need to have the token category attribute.
    //
    // Search for the Target Input among the inputs of this Genesis Transaction. If the Target Input cannot be found,
    // the UTXO should be filtered out. The Target Input must satisfy the following three conditions:
    //    It is a Genesis Input.
    //    It spends a covenant whose code is specified above.
    //    The symbol it reveals is the one we're querying.
    //
    // The canonical category is the token category defined by the first UTXO in this filtered list.

    let genesis_txid = token_genesis.txid();

    let reveal_txs = fetch_reveal_transactions(confirmed, &crc20.genesis_out).await?;
    let commit_txs = stream::iter(reveal_txs.iter())
        .then(|(txid, _h)| async move {
            let tx = get_tx_funding_prevout(confirmed, txquery, &compute_outpoint_hash(txid, 0))
                .await?
                .context(format!("no funding tx for reveal tx {}", txid))?;

            Ok((tx.0, tx.2))
        })
        .try_collect::<Vec<(Transaction, u32)>>()
        .await?;
    let commit_txs: Vec<(Transaction, u32)> = commit_txs
        .into_iter()
        .filter(|(tx, _)| is_genesis_tx(tx))
        .collect();

    // From spec: If this UTXO is not an output of the Genesis Transaction, filter it out.
    let mut txs: Vec<((Transaction, u32), (Txid, u32))> =
        commit_txs.into_iter().zip(reveal_txs.into_iter()).collect();

    txs.par_sort_unstable_by(|(commit_a, reveal_a), (commit_b, reveal_b)| {
        let fair_height_a = std::cmp::max(commit_a.1, reveal_a.1.saturating_sub(20));
        let fair_height_b = std::cmp::max(commit_b.1, reveal_b.1.saturating_sub(20));

        if fair_height_a == fair_height_b {
            // Order by little endian tx hash if height is the same,
            return commit_a.0.txid().cmp(&commit_b.0.txid());
        }
        fair_height_a.cmp(&fair_height_b)
    });

    Ok(match txs.first() {
        Some(((commit_tx, _), _)) => {
            if commit_tx.txid() == genesis_txid {
                Some(crc20)
            } else {
                None
            }
        }
        None => None,
    })
}

#[cfg(test)]
mod tests {
    #[cfg(bch)]
    use super::*;
    #[cfg(bch)]
    use bitcoincash::consensus::deserialize;

    #[test]
    // Token MESH with 8 decimals
    #[cfg(bch)]
    fn test_parse_crc20_mesh() {
        let tx_hex = "0200000001e380f9b4996d2e53affdbf20c53381db40a5dcf3e1c01a567acb4e2a15e0b61500000000a641aac8c0273591b431103c0fc9de0ecaa7dfd9282b25f150da794a6aec588e9465442fd3e936cae408e0c88ddb8fdfeee22e65fb32c3ed5605a9a3062d3f959943614c6254094d455348084d4553484104110c42ba7c4f08e820f3ee128cb6bdab952a9bde793d85c5c040b7ca8a204b1351a71f2c914ffb13622d19a0029ae0f5104df1c684ec9444af26c20719796bfe537a7cad7c7f75a90376a9147c7e0288ac7e00cd87feffffff0322020000000000001976a9148868b2b349e1817364385e161f034f53b71b175188ac9d0200000000000042efe380f9b4996d2e53affdbf20c53381db40a5dcf3e1c01a567acb4e2a15e0b61510ff00407a10f35a0000a9148ceab93cd88957876d5baca74bc18c1f1e2299d98700000000000000000a6a0800005af3107a4000a0280c00";
        let tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();

        let result = parse_token_crc20(&tx);
        assert!(result.is_some());
        let crc20 = result.unwrap();

        assert_eq!("MESH", crc20.symbol);
        assert_eq!(8, crc20.decimals);
        assert_eq!("MESH", crc20.name);
    }

    // Token testty
    #[test]
    #[cfg(bch)]
    fn test_parse_crc20_testty() {
        let tx_hex = "0200000001c042bfa5fd9162693b83ac266ae86c9ce5ac8940ed97475ccb724dd57d9b148800000000aa4119cd0adfc058402e75c5d7950bad73586ad6958a92f98a9edf99d46c015956da8330a7d4f6a930acee2d038f95c0ed1ffb63b20b8b47c310ae8f21b58e4b331a614c66560d746573747479037465737474794104a27d8aa3d5ffc9b28486d708ba6dea6d6fcefc18250fbc680c6872bbabf0457f9e242a6526099f9a34f2ab327150d17b929498f421f5a29e00178f0be6d0f332537a7cad7c7f75a90376a9147c7e0288ac7e00cd87feffffff0322020000000000001976a914ced85731c9749570369c21ef4ad096fac308f06f88ac9d0200000000000042efc042bfa5fd9162693b83ac266ae86c9ce5ac8940ed97475ccb724dd57d9b148810ff0080c6a47e8d0300a9148b9ca78d6a7eb4dd8a7605668d816ae1d951925e8700000000000000000a6a0800038d7ea4c680002d420c00";
        let tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();

        let result = parse_token_crc20(&tx);
        assert!(result.is_some());
        let crc20 = result.unwrap();

        assert_eq!("testty", crc20.symbol);
        assert_eq!(3, crc20.decimals);
        assert_eq!("testty", crc20.name);
    }

    #[test]
    #[cfg(bch)]
    fn test_parse_crc20_thd() {
        let tx_hex = "02000000018eed767c517f7bf577efdf8eb6dd4e67fcc81cedf5e205781c9f9e2c1acf5cf300000000b541469a30d3870e95a1b31e95d912dcfdcbb0fd503f26148ce0dd67310374c83a2a99410271da9b433e2be569de7b9be5a79871d17097c6ff2e326af16f3a3b50ac614c715318544448085468652044727567676564206875736b696573204104165a38b37a28559db1cadfbc3b77b6a6a53e87b580450d928480874e17fc5584b70bed4264f28a6c1cdd7962382f4d4dc825015c6bc3336ebf56643e183c937e537a7cad7c7f75a90376a9147c7e0288ac7e00cd87feffffff0322020000000000001976a914370e29e4d7850547bff83b223d38e9361a35d09c88ac9d0200000000000042ef8eed767c517f7bf577efdf8eb6dd4e67fcc81cedf5e205781c9f9e2c1acf5cf310ff00a0724e18090000a91489c12b49658e447d675b523c8cf40e527992664d8700000000000000000a6a08000009184e72a00073300c00";
        let tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();

        let result = parse_token_crc20(&tx);
        assert!(result.is_some());
        let crc20 = result.unwrap();

        assert_eq!("TDH", crc20.symbol);
        assert_eq!(8, crc20.decimals);
        assert_eq!("The Drugged huskies ", crc20.name);
    }
}