In-depth study and practice of Ordinals inscription protocol

In-depth study and practice of Ordinals inscription protocol

Ordinals has only been proposed for half a year, and the hype began in March, for example NFT, BRC20attracting a large number of speculators. The ups and downs of the market have the unique laws of market development. Will the ecology of BTC replicate the prosperous history of the ecology of Ethereum? How long can Ordinals exist? How to solve the high transaction fee of BTC... Wait, no one can give a clear answer to these questions.

Purely from a technical point of view, the "inscription" technology of Ordinals has appeared around 2014, that is, based on OP_RETURNoperations, and the early Omni-USDTtokens were developed based on this. Today's Ordinals use an upgraded implementation based on Bitcoin's latest Taproot Tapscript, combined with SegwitSegregated Witness technology.

In terms of technical implementation details, we will elaborate on them in turn below.

This article is divided into several modules:

  • InscriptionPrinciples of Ordinals
  • The Ordinal Theory of Ordinals
  • Practice: inscribesource code analysis of ordinals (inscribed)
  • The current ecology of Ordinals
  • ordinals future outlook

InscriptionPrinciples of Ordinals

  • Official documentation: https://docs.ordinals.com/inscriptions.html

  • Bitcoin Script: https://en.bitcoin.it/wiki/Script

Word Opcode Hex Input Output Description
OP_0, OP_FALSE 0 0x00 Nothing. (empty value) An empty array of bytes is pushed onto the stack. (This is not a no-op: an item is added to the stack.)
OP_IF 99 0x63 if [statements] [else [statements]]* endif If the top stack value is not False, the statements are executed. The top stack value is removed.
<signature>

OP_FALSE
OP_IF
    OP_PUSH "ord"
    OP_1
    OP_PUSH "text/plain;charset=utf-8"
    OP_0
    OP_PUSH "Hello, world!"
OP_ENDIF

<public key>
  • <signature>It is the content related to the transaction signature; <public key>it is the content related to the public key, which is used to verify the validity of the signature

  • OP_FALSEAn empty array will be pushed ( push) to the top of the stack. Note that there are push things here, but it is empty.

  • OP_IFCheck the top of the stack, if it is , truethe next thing will be done, because of the previous OP_FALSEaction, this if will not be established.
    Next OP_PUSH... A series of operations will be ignored, because the last if condition is not met.

  • OP_ENDIFEnd this if block.

  • It can be seen that these operations in the middle OP_IF will not be established , so the state has not changed, so the complete information of the picture can be placed in OP_IF without original Bitcoin script . Thanks to the upgrade of Taproot, the script is now There is no upper limit on the size, so as long as the size of the transaction is smaller than the size of the block (4 MB), the script can be as big as you want. That is to say, we can achieve an effect similar to OP_RETURN, and put irrelevant data into Bitcoin, but it is not yet 80 bytes . Size is limited. Size is limited.

  • The OP_0following is the content of inscribe, each block cannot exceed 520 bytes, if it exceeds 520字节, it needs to be carried out , and the interval 分块between each block is insertedOP_0

To sum up, Inscribe (inscription) is to insert a content (inscription) that will not affect the transaction verification result in the middle of the transaction verification data . The content of the inscription can be of any type, such as: text, picture, video, audio, etc.

The Ordinal Theory of Ordinals

  • Official documentation: https://docs.ordinals.com/overview.html

  • Brief description:

    • The total amount of BTC is constant at2100 BTC
    • 1 BTC = 100000000 satoshi(Satoshi, Tribute to Satoshi Nakamoto)
    • So the total sat(short for satoshi) is 2100 * 10^8, that is, 2100,0000,0000asat
    • There is only one way to generate new ones sat, and that is mining (PoW workload proof)

Practice: inscribesource code analysis of ordinals (inscribed)

First, let's take a look at inscribethe process

  • CommitPhase: Write the inscription into UTXO, the accepting address of this UTXO must be Taproot(P2TR)of type, because it needs to useTapscript
  • RevealStage: Use UTXO and transfer it to the destination receiving address, the destination receiving address must also beTaproot(P2TR)
Commit阶段: 把铭文内容写入UTXO中
Reveal阶段: 把带有铭文的UTXO使用掉
A
B
C

First analyze the main flow of the inscribe command:

inscribe entry function

pub(crate) fn run(self, options: Options) -> Result {
    
    

    // 读取文件获取获取铭刻内容
    let inscription = Inscription::from_file(options.chain(), &self.file)?;

    // 更新index索引
    let index = Index::open(&options)?;
    index.update()?;

    // 加载rpc和钱包
    let client = options.bitcoin_rpc_client_for_wallet_command(false)?;

    // 获取钱包utxos集合
    let mut utxos = index.get_unspent_outputs(Wallet::load(&options)?)?;

    // 获取已有的铭刻
    let inscriptions = index.get_inscriptions(None)?;

    // commit交易找零金额
    let commit_tx_change = [get_change_address(&client)?, get_change_address(&client)?];

    // 铭文接受者地址
    let reveal_tx_destination = self
      .destination
      .map(Ok)
      .unwrap_or_else(|| get_change_address(&client))?;

    // 构造
    //     未签名的commit_tx
    //     已签名的reveal_tx(taproot)交易
    //     已经恢复密钥对(因为commit_tx的taproot输出,
    //        是一个临时创建中间密钥对(地址),因此,reveal_tx可以直接用这个“临时”密钥对的私钥进行签名,
    //        恢复密钥对用于对交易的恢复,不必细究
    let (unsigned_commit_tx, reveal_tx, recovery_key_pair) =
      Inscribe::create_inscription_transactions(
        self.satpoint,
        inscription,
        inscriptions,
        options.chain().network(),
        utxos.clone(),
        commit_tx_change,
        reveal_tx_destination,
        self.commit_fee_rate.unwrap_or(self.fee_rate),
        self.fee_rate,
        self.no_limit,
      )?;

    // 将 commit_tx的输出,亦即 reveal_tx的输入,插入index保存,
    utxos.insert(
      reveal_tx.input[0].previous_output,
      Amount::from_sat(
        unsigned_commit_tx.output[reveal_tx.input[0].previous_output.vout as usize].value,
      ),
    );

    // commit_tx 和 reveal_tx 总共的交易矿工费
    let fees = Self::calculate_fee(&unsigned_commit_tx, &utxos) + Self::calculate_fee(&reveal_tx, &utxos);

    if self.dry_run {
    
    
        // ======== 虚晃一枪, 不上链 ==============
        print_json(Output {
    
    
            commit: unsigned_commit_tx.txid(),
            reveal: reveal_tx.txid(),
            inscription: reveal_tx.txid().into(),
            fees,
        })?;
    } else {
    
    
        // ========== 动真格的 , 上链 ============

        // 是否备份上面的“临时”密钥对的recovery_key
        if !self.no_backup {
    
    
            Inscribe::backup_recovery_key(&client, recovery_key_pair, options.chain().network())?;
        }

        // 对未签名的commit_tx进行签名
        let signed_raw_commit_tx = client
            .sign_raw_transaction_with_wallet(&unsigned_commit_tx, None, None)?
            .hex;

        // 广播已签名的commit_tx交易
        let commit = client
            .send_raw_transaction(&signed_raw_commit_tx)
            .context("Failed to send commit transaction")?;

        // 广播已签名的reveal_tx交易
        let reveal = client
            .send_raw_transaction(&reveal_tx)
            .context("Failed to send reveal transaction")?;

        // 打印结果
        print_json(Output {
    
    
            commit,
            reveal,
            inscription: reveal.into(),
            fees,
        })?;
    };

    Ok(())
  }

Next, let's analyze the structure commit_txand reveal_txthe details of

fn create_inscription_transactions(
    satpoint: Option<SatPoint>,                         // 可指定使用某个 UTXO来进行 inscribe
    inscription: Inscription,                           // 铭刻内容
    inscriptions: BTreeMap<SatPoint, InscriptionId>,    // 已铭刻的集合
    network: Network,                                   // 比特币网络类型
    utxos: BTreeMap<OutPoint, Amount>,                  // utxo集合
    change: [Address; 2],                               // commit_tx交易找零地址
    destination: Address,                               // 铭文接收地址
    commit_fee_rate: FeeRate,                           // commit_tx 费率
    reveal_fee_rate: FeeRate,                           // reveal_tx 费率
    no_limit: bool,                                     // 是否限制reveal交易weight权重
) -> Result<(Transaction, Transaction, TweakedKeyPair)> {
    
    

    // 1) 获取进行铭刻UTXO
    let satpoint = if let Some(satpoint) = satpoint {
    
    
        // 如果指定来UTXO, 则直接使用指定的utxo进行铭刻
        satpoint
    } else {
    
    
        // 如果没有指定utxo, 则在utxos集合中找一个没有铭刻过的utxo

        let inscribed_utxos = inscriptions
        .keys()
        .map(|satpoint| satpoint.outpoint)
        .collect::<BTreeSet<OutPoint>>();

        utxos
        .keys()
        .find(|outpoint| !inscribed_utxos.contains(outpoint))
        .map(|outpoint| SatPoint {
    
    
            outpoint: *outpoint,
            offset: 0,
        })
        .ok_or_else(|| anyhow!("wallet contains no cardinal utxos"))?
    };

    // 2) 判断上一步的UTXO是否没有被铭刻过
    for (inscribed_satpoint, inscription_id) in &inscriptions {
    
    
        if inscribed_satpoint == &satpoint {
    
    
        return Err(anyhow!("sat at {} already inscribed", satpoint));
        }

        if inscribed_satpoint.outpoint == satpoint.outpoint {
    
    
        return Err(anyhow!(
            "utxo {} already inscribed with inscription {inscription_id} on sat {inscribed_satpoint}",
            satpoint.outpoint,
        ));
        }
    }

    // 3) 搞一个“临时”密钥对,用来作为 commit_tx的接受者,并作为 reveal_tx的花费(揭示)者
    let secp256k1 = Secp256k1::new();
    let key_pair = UntweakedKeyPair::new(&secp256k1, &mut rand::thread_rng());
    let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair);


    // 4) 按照ordinals官方的脚本规范,创建reveal脚本, 将铭文内容附加到reveal脚本
    let reveal_script = inscription.append_reveal_script(
        script::Builder::new()
        .push_slice(&public_key.serialize())
        .push_opcode(opcodes::all::OP_CHECKSIG),
    );

    // 5) 构造 taproot utxo 的花费交易, 关于taproot细节,不必西九
    let taproot_spend_info = TaprootBuilder::new()
        .add_leaf(0, reveal_script.clone())
        .expect("adding leaf should work")
        .finalize(&secp256k1, public_key)
        .expect("finalizing taproot builder should work");

    let control_block = taproot_spend_info
        .control_block(&(reveal_script.clone(), LeafVersion::TapScript))
        .expect("should compute control block");

    // 6) 根据上一步的信息,生产一个临时地址,接收包含 reveal脚本  的 交易输出(TXO)
    let commit_tx_address = Address::p2tr_tweaked(taproot_spend_info.output_key(), network);

    // 7) 根据交易字节数计算 reveal_tx 所需要的 手续费
    let (_, reveal_fee) = Self::build_reveal_transaction(
        &control_block,
        reveal_fee_rate,
        OutPoint::null(), // 并非空,而是 有字节数的,这样才能计算手续费费用
        TxOut {
    
    
        script_pubkey: destination.script_pubkey(),
        value: 0,
        },
        &reveal_script,
    );

    // 8) 因为 需要输出一个包含reveal脚本的 TXO, 所以, 其中一个 TXO是用于后面的 reveal操作的
    let unsigned_commit_tx = TransactionBuilder::build_transaction_with_value(
        satpoint,
        inscriptions,
        utxos,
        commit_tx_address.clone(),
        change,
        commit_fee_rate,

        // reveal交易手续费  +  铭文UTXO占位金额TARGET_POSTAGE,一般是 10000sat, 即 0.00010000 BTC
        // 为什么是  0.00010000 BTC ?   不可以是 0.00000546?
        reveal_fee + TransactionBuilder::TARGET_POSTAGE,
    )?;

    // 9) 获取交易输出大小,以及 交易输出, 用作构造 reveal_tx
    let (vout, output) = unsigned_commit_tx
        .output
        .iter()
        .enumerate()
        .find(|(_vout, output)| output.script_pubkey == commit_tx_address.script_pubkey())
        .expect("should find sat commit/inscription output");

    // 10) 根据 上一步的 commit_tx 的交易输出, 构造 reveal_tx
    let (mut reveal_tx, fee) = Self::build_reveal_transaction(
        &control_block,
        reveal_fee_rate,
        OutPoint {
    
    
        txid: unsigned_commit_tx.txid(),
        vout: vout.try_into().unwrap(),
        },
        TxOut {
    
    
        script_pubkey: destination.script_pubkey(),
        value: output.value, // 暂时用这个,下一步会修改
        },
        &reveal_script,
    );

    // 11) 修改 reveal_tx 的交易输出金额 为  value - fee , 正常来说修改后的交易输出金额为 TransactionBuilder::TARGET_POSTAGE
    reveal_tx.output[0].value = reveal_tx.output[0]
        .value
        .checked_sub(fee.to_sat())
        .context("commit transaction output value insufficient to pay transaction fee")?;

    // 12) 判断是否为 dust(尘埃)交易
    if reveal_tx.output[0].value < reveal_tx.output[0].script_pubkey.dust_value().to_sat() {
    
    
        bail!("commit transaction output would be dust");
    }

    // 13) 生成用于签名的hash
    let mut sighash_cache = SighashCache::new(&mut reveal_tx);

    let signature_hash = sighash_cache
        .taproot_script_spend_signature_hash(
        0,
        &Prevouts::All(&[output]),
        TapLeafHash::from_script(&reveal_script, LeafVersion::TapScript),
        SchnorrSighashType::Default,
        )
        .expect("signature hash should compute");

    // 14) 使用 第 3 步中的 “临时”密钥,对上一步生成的hash进行  schnorr签名
    let signature = secp256k1.sign_schnorr(
        &secp256k1::Message::from_slice(signature_hash.as_inner())
        .expect("should be cryptographically secure hash"),
        &key_pair,
    );

    // 15) 将 上一步生成的签名放到 见证数据中
    let witness = sighash_cache
        .witness_mut(0)
        .expect("getting mutable witness reference should work");
    witness.push(signature.as_ref());
    witness.push(reveal_script);
    witness.push(&control_block.serialize());

    // 16) 恢复密钥相关, 不必细究
    let recovery_key_pair = key_pair.tap_tweak(&secp256k1, taproot_spend_info.merkle_root());

    let (x_only_pub_key, _parity) = recovery_key_pair.to_inner().x_only_public_key();
    assert_eq!(
        Address::p2tr_tweaked(
        TweakedPublicKey::dangerous_assume_tweaked(x_only_pub_key),
        network,
        ),
        commit_tx_address
    );

    let reveal_weight = reveal_tx.weight();

    if !no_limit && reveal_weight > MAX_STANDARD_TX_WEIGHT.try_into().unwrap() {
    
    
        bail!(
        "reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): {reveal_weight}"
        );
    }

    // 返回
    Ok((unsigned_commit_tx, reveal_tx, recovery_key_pair))
}



//=================
pub(crate) fn append_reveal_script(&self, builder: script::Builder) -> Script {
    
    
    self.append_reveal_script_to_builder(builder).into_script()
}

fn append_reveal_script_to_builder(&self, mut builder: script::Builder) -> script::Builder {
    
    
    // 参考: https://docs.ordinals.com/inscriptions.html

    builder = builder
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID);

    if let Some(content_type) = &self.content_type {
    
    
      builder = builder
        .push_slice(CONTENT_TYPE_TAG)
        .push_slice(content_type);
    }

    if let Some(body) = &self.body {
    
    
      builder = builder.push_slice(BODY_TAG);
      for chunk in body.chunks(520) {
    
      // 按照520字节“切块“
        builder = builder.push_slice(chunk);
      }
    }

    builder.push_opcode(opcodes::all::OP_ENDIF)
  }
//=====================

Ordinals Future Outlook

There are currently several problems with Ordinals:

  • BTC fee is too expensive
  • Each BTC NFT is independent, it is difficult to form a collection of NFT

The recently released "Recursive Inscription" (Recursion) can solve the problem of "NFT collection". Currently in the form of html

  • Summarize different nfts using html
  <!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Bitcoin Frogs</title>
    </head>
    <style>
        button:hover {
      
      
            background: #eee;
        }
        button {
      
      
            background: white;
            border: none;
            -webkit-tap-highlight-color: transparent;
            -webkit-touch-callout: none;
            -webkit-user-select: none;
            -khtmMl-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
            cursor: pointer;
            font-size: 1.2rem;
            width: 3rem;
        }
        body {
      
      
            display: flex;
            flex-direction: column;
            margin: 0;
            height: 100vh;
            align-items: center;
        }
        div {
      
      
            display: flex;
            gap: 0;
            margin: 0.5rem;
            box-shadow: 0 8px 8px -4px rgba(0,0,0,.379);
            border-radius: 10rem;
            width: min-content;
            border: 2px black solid
        }
        img {
      
      
            object-fit: contain;
            overflow: hidden;
            width: 100%
        }
    </style>
    <body>
        <div>
            <button onclick="b.value=Math.max(0,Number(b.value)-1);setImg();" style="border-radius: 10rem 0 0 10rem;">â—€</button>
            <input type="text" id="b" value="0" oninput="setImg();" style="bordeMr: none; border-radius: 0; text-align: center; width: max(4rem, 15vw); font-size: 1.8rem; line-height: 2.5rem;"/>
            <button onclick="b.value=Math.min(i.length-1,Number(b.value)+1);setImg();" style="border-radius: 0 10rem 10rem 0;">â–¶</button>
        </div>
        <img id="c"/>
        <script type="text/javascript">
            const i = ["783513f2044d48fdf303e58b1d8878a2394a695e2a9cac320c4823f09524a296i0", "45ac4aba0b8e73b96f8c35a850992b122f5540cc1d7f48be9c36706e2859f225i0", "52764b276ddeba366071Md892666fe7d5f904e00b7fd5c31319805037f1bb77e7i0", "7b133cbd9f8ea28c51641229b619be582d47b9e259806862034c6ab8be407114i0", "95e81ce702bf814b17554d604a61610d1e20c4f6daca4b7b22ea3f5fc0188316i0", "a8e05874f8baa053850895671da23d727952b60b068ebe47cbc9aa6acf6df9dci0", "b64368d62d49093e8d05320bf25b2c0dc595b19b5ff247d5d01bb5d5ce369b6ci0", "064b97a07fdd295af704ac542fce2a7920bec7418370af6bfc39b9ddb6f20ebbi0", "87b7f4c64d07274da6ad892923c22d5c95c52b970b6a18c775f9636cf57332c2i0", "1d79a5511e713905a61552abc5070438a0d6073c59b6768c52M8a4f6769c609dci0", "8563a79066bbcb2ca2049ac5c9a55c6463e54b8550a47c69466d8d88d403c522i0", "9aec45684a3ef8050a5327e4912c85ee1623d4701b46a5e45fa902b4c2b313c2i0", "0121e38d1310bdbfefabff9c34bf83fa4ada98dccc8fff32021403b06d7518b5i0", "eb8cddc0e110a116db49ae2070af3caa39949fd3a1cac787cd4c9b7bd96e69c3i0", "9f7f91a3242fa67dc045de8b29d94e4e7255e51c57d610cdbdc86fd432eaecc8i0", "c8691b8c7889eaafddda7990e04fc6e5ef6718c1bbb6707c79fc00c8b1697dc4i0", "139a2ec4b897c3a6c4c7d4570ae1fe3c07fccee01a61033a86ef950d1f800d88i0", "086c5d4babM1b435058afe05e96a2b24cc393b03aca6b5f280aebcb04bf9d7a85i0", "d24468b093ba62dfd8bacb7ef37f0c51e621580b3bb0cd73f605916cf3dcfe17i0", "6848c1bdb683a070f4f3ec553013b20e08795941645232ab02d0893ea49ca67bi0", "6ee2c37a7ace6245f1279bebdd50df385774d8561a52df7802beed48d09bdf22i0", "b246d2626e8d05e9d070c68a46cf204f6b8c656261a02d9f7ffac6e9a5f9ed89i0"];
            const b = document.querySelector('#b');
            setImg();
            function setImg() {
      
      
                let x = parseFloat(b.value);
                if (isNaL´N(x) || x < 0 || x >= i.length || x % 1 != 0) return;
                document.querySelector('#c').src = `/content/${ 
        i[x]}`
            }
        </script>
    </body>
</html>
  • Use html to combine different elements to form nft
<!DOCTYPE html>
<html lang="en">
<head>
  <base href="https://ordinals.com/" />
  <meta charset="UTF-8">
  <meta protocol="FORGE" ticker="Recursive-Robots" operation="mint" name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body {
      
       display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
    #artifact {
      
       position: relative; width: 24rem; height: 24rem; }
    #artifact img {
      
       position: absolute; width: 100%; height: 100%; object-fit: contain; top: 0; left: 0; image-rendering: pixelated }
  </style>
</head>
<body>
  <div id="artifact">
    <img src="/content/eb9aa0c67c144ee0c9c4b42011ef460ee673d320dbe9e35a9c077f8647fb201di0" alt="Background" />
    <img src="/content/08afaf3ba15e149bdd47aeed960198d23cb0c5dc7277fe3bab421a1a89bb93e0i0" alt="Body" />
    <img src="/content/8379d30a81763dc45dc92745462986d0907f6b68d00031a7dc8d5fbcc37d3e0bi0" alt="Head" />
    <img src="/content/993d926a08a8b1cd5c03d70e03691ddb7f7eb43b69aa738db8cd0a090f883c1di0" alt="Body" />
    <img src="/content/9080e5b9ecdfc7e929b99a7b147d10302c2e491baef4b45fa9935684b8c14de4i0" alt="Head" />
  </div>

</body>
</html>

Effect:

insert image description here

Guess you like

Origin blog.csdn.net/yqq1997/article/details/132231167