rostrum 14.0.1

An efficient implementation of Electrum Server with token support
Documentation
use std::convert::TryInto;

use anyhow::Result;
use bitcoincash::Txid;
use serde::Deserialize;

use crate::utilserialize::as_hex;
use crate::{
    chaindef::Transaction,
    encode::compute_outpoint_hash,
    query::{queryutil::tx_spending_outpoint, tx::TxQuery},
    store::DBStore,
    utilscript::read_push_from_script,
};

#[derive(Debug, Serialize, Deserialize)]
pub struct BCMR {
    #[serde(serialize_with = "as_hex")]
    pub hash: [u8; 32],
    pub uris: Vec<String>,
}

pub fn parse_bcmr(tx: &Transaction) -> Option<BCMR> {
    let bcmr_op_return = tx.output.iter().find(|o| {
        o.script_pubkey.as_bytes().starts_with(&[
            0x6a, // OP_RETURN
            0x04, // PUSH 4
            0x42, // B
            0x43, // C
            0x4d, // M
            0x52, // R
        ])
    })?;

    let iter = bcmr_op_return.script_pubkey[6..].iter();
    let (iter, hash) = read_push_from_script(iter).ok()?;

    let hash: [u8; 32] = hash.and_then(|h| h.try_into().ok())?;
    let mut uris: Vec<String> = vec![];
    let mut uri_element;
    let mut uri_iter = iter;
    loop {
        (uri_iter, uri_element) = match read_push_from_script(uri_iter) {
            Ok(r) => r,
            Err(_) => break,
        };
        match uri_element {
            Some(url) => {
                if let Ok(uri) = String::from_utf8(url) {
                    uris.push(uri)
                }
            }
            None => break,
        }
    }

    Some(BCMR { hash, uris })
}

pub async fn get_authchain(
    token_genesis: &Txid,
    confirmed: &DBStore,
    unconfirmed: &DBStore,
) -> Vec<Txid> {
    let mut authchain = vec![*token_genesis];
    let mut authhead_outpoint = compute_outpoint_hash(token_genesis, 0);
    loop {
        let input_row = match tx_spending_outpoint(confirmed, &authhead_outpoint).await {
            Some(i) => Some(i),
            None => tx_spending_outpoint(unconfirmed, &authhead_outpoint).await,
        };

        match input_row {
            Some(row) => {
                authhead_outpoint = compute_outpoint_hash(&row.txid(), 0);
                authchain.push(row.txid());
            }
            None => {
                // Authchain is complete
                break;
            }
        }
    }
    authchain
}

pub async fn get_token_bcmr(
    token_genesis: &Txid,
    confirmed: &DBStore,
    unconfirmed: &DBStore,
    txquery: &TxQuery,
) -> Result<Option<BCMR>> {
    for txid in get_authchain(token_genesis, confirmed, unconfirmed)
        .await
        .into_iter()
        .rev()
    {
        let tx = txquery.get(&txid, None, None, false).await?;
        if let Some(bcmr) = parse_bcmr(&tx) {
            return Ok(Some(bcmr));
        }
    }
    Ok(None)
}

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

    use super::*;

    #[test]
    fn test_parse_bcmr() {
        let tx_hex = "0200000001925bd92e424c0f0bc290a794f491abf19e61b6dcdec7e70747fcb54682fc9bb700000000644181707613f45069c8df981b2ef0cbd05158d5623602fac3ae2e13f69d05fa356c278f2f3a4e34f33a79e0f8375abd35051150ec024b3625807cea185528a5de9e412103b4680dffa1e34b3bfdb024fd0a8498eb24f59ba5ead34acfb74f9d8549db6f3a0000000003e8030000000000003eef925bd92e424c0f0bc290a794f491abf19e61b6dcdec7e70747fcb54682fc9bb710fdf40176a914bd4c90f2c64743fc0d3ea6a14973a5d628260b7388ac0000000000000000456a0442434d5220c705cc90a56ac7ef9a15ef90ebbc8ba7e60e4c622e5464d52d8baf7887949fcc1d736f636b2e6361756c64726f6e2e71756573742f62636d722e6a736f6ed9210000000000001976a914bd4c90f2c64743fc0d3ea6a14973a5d628260b7388ac00000000";
        let tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();

        let result = parse_bcmr(&tx);
        assert!(result.is_some());
        let bcmr = result.unwrap();

        assert_eq!(
            hex::encode(bcmr.hash),
            "c705cc90a56ac7ef9a15ef90ebbc8ba7e60e4c622e5464d52d8baf7887949fcc"
        );
        assert_eq!(bcmr.uris.len(), 1);
        assert_eq!(bcmr.uris[0], "sock.cauldron.quest/bcmr.json");
    }

    #[test]
    fn invalid_bcmr_hash() {
        // txid 4e44c45cf1fb956fe4752c6dfa9637f29cf18e9251e20454dd65dbcc298617b6
        let tx_hex = "0200000002020056e81cda3b55f6b912acf05131e4f50594dc76702e8022d5522582f74bd40000000064414048069ab85d72e0f195e6587a4ef30360d5304a7d24995e21f8bb3d9c9c52eb5d1408a3a4754fad8da6593dc65fd248e507fe1620004bcb87f3237fcea9679e4121028bb6fdf7137233695237a6fa538827afb4f7490bad82215098b40dc3d9c645fc00000000020056e81cda3b55f6b912acf05131e4f50594dc76702e8022d5522582f74bd4010000006441d501d321b457b2622388efc9290245e7021f082d2914bf1883184ea29f546a2b08d74537f848fba17d41e44ad6f9d6949b44670b379635b00fe69caa5236e47f4121028bb6fdf7137233695237a6fa538827afb4f7490bad82215098b40dc3d9c645fc0000000003e80300000000000042ef020056e81cda3b55f6b912acf05131e4f50594dc76702e8022d5522582f74bd431ff00b402869d7e0100a9147ca730ccf0ba8c552bae1287d01a9e7e5590f0fa870000000000000000d56a0442434d5240646331303939346363323530363731643830383737626639356131613136616164356431343033623365623263373136346366623231383532323363353162664c506e667473746f726167652e6c696e6b2f697066732f6261666b726569673463636d757a7173716d346f796262333337666e627566766b32786975616f7a36776c64726d746833656763736570637278343b6261666b726569673463636d757a7173716d346f796262333337666e627566766b32786975616f7a36776c64726d74683365676373657063727834bb0f0000000000001976a914d948abf743a76f472703b6a91437854dc1d7ced588ac00000000";
        let tx: Transaction = deserialize(&hex::decode(&tx_hex).unwrap()).unwrap();
        let result = parse_bcmr(&tx);
        // The hash value in this BCMR is 64 bytes. Not a valid sha256 hash.
        assert!(result.is_none());
    }
}