比特幣入門

9龍發表於2020-07-05

 

 

引言

上一篇我們已經知道了什麼是區塊鏈,此篇說一下區塊鏈的第一個應用——比特幣。其實先有比特幣,後有的區塊鏈。比特幣是中本聰提出的一種P2P去中心化的支付系統,其底層原理為:去中心化、資料不可篡改、可溯源;資料以區塊的形式儲存,並且像鏈條一樣連線起來,很形象地稱為區塊鏈。比特幣就是一條上文說過的公有鏈。

比特幣基本結構

上一節我們說過,賬本以交易的形式記錄,那交易是如何儲存在區塊鏈上呢?

交易儲存在區塊之中。區塊分為區塊頭以及區塊體,區塊頭包含區塊的概要資訊,區塊體包含交易資訊。

  • 區塊頭
  1. version(版本號):用於協議區分或者升級
  2. height:區塊高度
  3. hash(區塊Hash):區塊的唯一指紋
  4. preBlockHash(父區塊Hash):用於向前追溯
  5. merkleRoot(默克爾樹根Hash):可用於節點快速驗證區塊的交易是否被篡改或者SPV(Simplified Payment Verification 簡單支付驗證)客戶端驗證某一個交易是否存在於比特幣鏈上。
  6. time(區塊生成時間)
  7. dificultyTarget(目標難度:nBits):挖礦難度
  8. nonce(隨機數):比特幣協議中“難題的解”
  • 區塊體
  1. transactions(區塊包含的交易列表):被礦工打包在區塊中的交易,實際為使用者的轉賬交易。
    • version(版本號):用於升級
    • Inputs:交易輸入列表,指向待花費的UTXO,可以有零個至多個。coinbase(創幣交易)沒有輸入。
    • Outputs:交易輸出列表:指向即將產生的UTXO
    • lockTime:交易鎖定時間。實際為UNIX時間戳(相對於1970年1月1日0點後流逝的秒數。)礦工打包交易時,會判斷是否到達此時間,到達後才會將此交易進行打包。
    • time:交易的生成時間
    • memo:備註欄位,使用者可自由填寫。

比特幣入門

比特幣基礎知識

1)雜湊演算法

雜湊演算法就是將任意長度的資料轉變為一個定長的資料(也叫雜湊或者摘要)。常見的雜湊演算法有CRC32、MD5、SHA1、SHA2、SHA3、Hmac等。

雜湊演算法具有以下特點:

  • 單向不可逆性:想要通過雜湊值逆推出原來的資料,只有通過暴力破解的方法實現,但這幾乎無法做到,可能量子計算機出來後就可以破解了。

  • 確定性:如果hash值不同, 可以確定原資料一定不同。如果相同,則還需要進一步判斷,因為可能hash碰撞了。

  • 雪崩效應:原始資料任何微小的變動都會導致雜湊值完全不一樣

  • 不變性:同一個資料,多次hash結果相同。

2)非對稱加密技術

非對稱加密有公鑰和私鑰兩個概念,私鑰自己擁有,不能給別人,公鑰公開。根據應用的不同,我們可以選擇使用不同的金鑰加密:

  • 私鑰簽名:使用私鑰簽名(加密),公鑰驗籤(解密)。用於讓公鑰所有者驗證私鑰所有者的身份並且用來防止私鑰所有者釋出的內容被篡改,但是不用來保證內容不被他人獲得。(內容本身非加密的,別人可以獲得資訊但無法篡改)

  • 公鑰加密:用公鑰加密,私鑰解密。公鑰所有者可用於加密釋出的資訊,這個資訊可能被他人篡改,但是無法被他人獲得。

    ​ 舉例:比如小黑持有公鑰,將 “我愛你” 加密後的訊息 “xxx” 傳送給喜歡的人小花(小花持有私鑰),但小灰也喜歡小花並且也有公鑰,小灰劫持了小黑的訊息(訊息內容已加密,小灰也不知道內容是什麼),並將 “我恨你” 加密為 “xxx”組裝為小黑的請求傳送給小花,小花收到小黑的請求後,用私鑰解密,得到內容為:“我恨你”。這下小黑就涼涼,心機boy小灰就成功了。

    簽名與加密可以混合使用,提高安全性與隱私性。

3)數字簽名

作用:保證資訊傳輸的完整性、傳送者的身份驗證(鑑權)

原理:使用非對稱加密技術及hash技術,非對稱加密生成一對公私鑰對,私鑰用於簽名,公鑰用於驗籤。簽名的物件為內容的hash。

為什麼簽名不直接簽署內容,而是簽署hash呢?

答:效率問題,如果你的原內容很大,直接對原內容簽名效率很低,而且簽名的結果也相對很大,傳輸也慢。

簽名驗簽過程

  • 簽名過程:首先獲取通過雜湊演算法獲取原文的摘要資訊,然後用私鑰加密,並將原文一起傳送給接受者。
  • 驗證過程:接收者只有使用傳送者的公鑰才能解密被加密的摘要資訊,同樣通過雜湊演算法獲取原文的摘要資訊,並與解密後的摘要資訊做對比。如果相同則說明資訊完整,確實是對方本人的簽名(因為公鑰和私鑰是一對的)。

在比特幣系統中,公鑰(地址)用於接收比特幣,而私鑰則用於比特幣支付時的交易簽名。

在支付比特幣時,比特幣的所有者需要在交易中提交自己的公鑰和該交易的簽名。而比特幣網路中所有節點可以通過所提交的公鑰和簽名進行驗證,從而確認支付者對交易的比特幣的所有權。

4)MerkleTree

Merkle樹是⼀種雜湊⼆叉樹,它是⼀種⽤作快速歸納和校驗⼤規模資料完整性的資料結構。這種⼆叉樹包含加密雜湊值。

在⽐特幣⽹絡中,Merkle樹被⽤來歸納⼀個區塊中的所有交易,同時⽣成整個交易集合的hash,且提供了⼀種校驗區塊是否存在某交易的⾼效途徑。

當N個資料元素經過加密後插⼊Merkle樹時,時間複雜度為log(N)【因為Merkle樹可能為完全二叉樹或者滿二叉樹。而滿二叉樹的高度為log(N)】就能檢查出任意某資料元素是否在該樹中,這使得該資料結構⾮常⾼效。

計算merkleRoot時,如果交易數量為奇數,則複製最後一個交易hash進行計算。

Merkle樹的效率

交易數量區塊的近似大小路徑大小(Hash數量)路徑大小(位元組)
16筆交易 4KB 4個Hash 128位元組
512筆交易 128KB 9個Hash 288位元組
2048筆交易 512KB 11個Hash 352位元組
65535筆交易 16MB 16個Hash 512位元組

注:單個Hash32位元組

依表可得,當區塊⼤⼩由16筆交易(4KB)急劇增加⾄65,535筆交易(16MB)時,為證明交易存在的Merkle路徑⻓度增⻓極其緩慢,僅僅從128位元組到512位元組。有了Merkle樹,⼀個節點能夠僅下載區塊頭(80位元組/區塊),然後通過從⼀個滿節點回溯⼀條⼩的Merkle路徑就能認證⼀筆交易的存在,⽽不需要儲存或者傳輸⼤量區塊鏈中⼤多數內容,這些內容可能有⼏個G的⼤⼩。這種不需要維護⼀條完整的區塊鏈的節點,⼜被稱作簡單⽀付驗證(SPV)節點,它不需要下載整個區塊⽽通過Merkle路徑去驗證交易的存在。

Merkle樹和簡單⽀付驗證(SPV)

Merkle樹被SPV節點⼴泛使⽤。SPV節點不儲存所有交易也不會下載整個區塊,僅僅儲存區塊頭。它們使⽤認證路徑或者Merkle路徑來驗證交易存在於區塊中,⽽不必下載區塊中所有交易。

區塊頭只有80位元組,而一個區塊大小為1M(比特幣已擴容為2M),所以SPV節點驗證交易是否存在,只需要比特幣區塊容量的千分之一的資料就可以,大大節省容量。

例子:使用上面的merkelTree中的交易。假設SPV節點想驗證交易A是否存在與區塊內。

如圖所示,SPV節點只需要獲取該交易所在的區塊頭以及驗證路徑:即N1、N4即可。

驗證步驟:
  1. 計算交易A的Hash得到N0
  2. 計算出N3的Hash = Hash(N0+N1)
  3. 計算merkleRoot = Hash(N3+N4)
  4. 比較計算的merkleRoot與區塊頭的merkleRoot是否相同,相同則表明該交易A存在於此區塊中。

注意:SPV只能驗證某交易是否存在與區塊中,而無法驗證該交易的UTXO是否雙花,需要等待該區塊後是否累積了多個區塊,越多證明該交易被大多數人共識確認,越無法篡改。比特幣中,6個區塊就可以確認該交易基本無法篡改,篡改的機率很低,並且成本昂貴。

為什麼比特幣的區塊大小為1M?

//For now it exists as an anti-DoS measure to avoid somebody creating a titanically huge but valid block and forcing everyone to download/store it forever.
public static final int MAX_BLOCK_SIZE = 1 * 1000 * 1000;

原始碼裡有註釋:預防dos攻擊,防止有節點建立很大且有效的區塊傳送到比特幣網路,這樣大家都會去驗證並廣播,造成網路擁堵。

5)哪有比特幣,有的是UTXO

什麼是UTXO?

UTXO:unspent transaction output 未花費的交易輸出。如果單看比特幣,其實就是UTXO,擁有多少比特幣,實質是你的UTXO集合有多少。一個交易輸出就是一個UTXO。

我們再看一下交易的結構

TransactionOutput

  1. txHash :交易的hash

  2. Index : 交易的輸出列表中的下標,從0開始。

  3. Value : 轉賬金額,比特幣最小單位為聰,1Btc = 10^8聰,轉賬單位也為聰

  4. lockScpript:鎖定指令碼,通常為地址。(表示轉賬的比特幣儲存的形式)

    • 指令碼型別

       public enum ScriptType {
              P2PKH(1), // pay to pubkey hash (aka pay to address)通常為此形式,支付到地址
              P2PK(2), // pay to pubkey
              P2SH(3), // pay to script hash
              P2WPKH(4), // pay to witness pubkey hash
              P2WSH(5); // pay to witness script hash
      
              public final int id;
              private ScriptType(int id) {
                  this.id = id;
              }
          }
      

交易輸出表述為:轉賬給哪一個地址的金額,位於交易輸出列表中的哪個位置。

TransactionInput

  1. TransactionOutpoint:表示引用了哪一個UTXO。使用之前的txHash以及index找到唯一的UTXO。
    1. txHash :之前交易的hash
    2. Index : 之前交易的輸出列表中的下標,從0開始。
  2. unlockScpript:解鎖指令碼:包含公鑰及簽名。
    1. pubKey:公鑰:用於驗籤
    2. Signature :簽名,用於比特幣網路中的礦工驗證該交易的發出者身份,是否有權利花費輸入中引用的UTXO。

6)比特幣挖礦與共識

挖礦是增加⽐特幣貨幣供應的⼀個過程。挖礦同時還保護著⽐特幣系統的安全,防⽌欺詐交易,避免“雙花” ,“雙花”是指多次花費同⼀筆⽐特幣。

**挖礦:指比特幣網路中的節點將網路中收到的合法交易進行打包,生成區塊的過程。**並且,交易列表的第一筆交易礦工會生成一個coinbase交易,即為創幣交易。(此交易沒有輸入,只有輸出,輸出到自己地址作為挖礦獎勵)礦⼯通過創造⼀個新區塊得到的⽐特幣數量⼤約每四年(或準確說是每210,000個塊)減少⼀半。開始時為2009年1⽉每個區塊獎勵50個⽐特幣,然後到2012年11⽉減半為每個區塊獎勵25個⽐特幣。之後將在2016年的某個時刻再次減半為每個新區塊獎勵12.5個⽐特幣。基於這個公式,⽐特幣挖礦獎勵以指數⽅式遞減,直到2140年。屆時所有的⽐特幣(20,999,999.98)全部發⾏完畢。換句話說在2140年之後,不會再有新的⽐特幣產⽣。現在(2020/7/02)為6.25BTC。

靈魂拷問:

比特幣網路中中的節點那麼多,大家都可以挖礦?而最終確定的區塊只有一個或少數幾個,全網這麼多礦工,大家怎麼都承認是你的區塊被接受,我的不被接受呢?

比特幣中有一個挖礦難度,這個難度可以轉換為一個256bit的大數,比特幣的共識為:全網的礦工都去打包區塊,但有一個條件,區塊的hash轉換的256bit的大數一定要不大於比難度轉換的才行。(也就是大家常說的計算的Hash前置的0多於目標值Hash的前置0一個意思

protected boolean checkProofOfWork(boolean throwException) throws VerificationException {
  //將難度轉換為一個256bit的大數      
  BigInteger target = getDifficultyTargetAsInteger();
  //區塊hash轉換為一個256bit的大數
        BigInteger h = getHash().toBigInteger();
  //如何區塊的數大於目標的,則不合法
        if (h.compareTo(target) > 0) {
            // Proof of work check failed!
            if (throwException)
                throw new VerificationException("Hash is higher than target: " + getHashAsString() + " vs "
                        + target.toString(16));
            else
                return false;
        }
        return true;
    }

如何計算Hash?

void writeHeader(OutputStream stream) throws IOException {
        //版本
        Utils.uint32ToByteStreamLE(version, stream);
     //父區塊Hash
        stream.write(prevBlockHash.getReversedBytes());
     //交易merkleTree hash
        stream.write(getMerkleRoot().getReversedBytes());
     //交易時間
        Utils.uint32ToByteStreamLE(time, stream);
     //區塊難度
        Utils.uint32ToByteStreamLE(difficultyTarget, stream);
     //挖礦的嘗試次數
        Utils.uint32ToByteStreamLE(nonce, stream);
    }

可以看出,區塊的hash中,變化的量有交易的merkleRoot、區塊時間、區塊nonce。但如果挖礦時交易列表已經確定,區塊時間也是確定的,那就只有嘗試更改nonce來更改區塊hash,以達到在該難度下的合法區塊hash。

 public void solve() {
        while (true) {
            try {
                // 工作量證明
                if (checkProofOfWork(false))
                    return;
                // 增加nonce,重新計算
                setNonce(getNonce() + 1);
            } catch (VerificationException e) {
                throw new RuntimeException(e); // Cannot happen.
            }
        }
    }

可以看到比特幣挖礦的過程就是尋找一個合法nonce的過程。

我們再來看一看,UTXO不是隻有唯一的一個嗎?為什麼還會產生雙花?

其實雙花在比特幣中對應於分叉。因為比特幣網路中的節點都是P2P的對等節點,每個節點打包的交易可能不同,時間也不同,nonce也不同。最終計算的區塊hash也不同。這樣就會在同一高度產生多個合法區塊,這就是分叉。

可能你的同一筆交易被打進了高度為101的兩個區塊中,分別對應於兩條鏈。只要被打進區塊中的交易,證明都是被礦工確認共識過後的合法交易,所以如果是一個作惡的使用者,則可以利用比特幣的分叉進行雙花攻擊。

比特幣網路為了避免雙花,引入了一個確認的機制。需要6個區塊才能確認一個區塊中的交易是否真正有效。(6個區塊是估算的)

7)區塊驗證(共識)

區塊驗證的過程就是共識的過程:驗證區塊是否按照比特幣協議產生的,是否都打包的合法交易,是否找到了合法nonce等。

  1. 每個全節點依據綜合標準對每個交易進⾏獨⽴驗證

    ▷交易的語法和資料結構必須正確。 ▷輸⼊(coinbase交易除外)與輸出列表都不能為空。

    ▷交易的位元組⼤⼩是⼩於 MAX_BLOCK_SIZE(當前1M) 的。 ▷每⼀個輸出值,以及總量,必須在規定值的範圍內 (⼩於2,100萬個幣,⼤於0)。 ▷交易的位元組⼤⼩是⼤於或等於100的。

    ▷交易的位元組⼤⼩最大為100kb。 ▷交易中的簽名數量應⼩於簽名運算元量上限。 ▷解鎖指令碼( scriptSig )只能夠將數字壓⼊棧中,並且鎖定指令碼( scriptPubkey )必須要符合 isStandard 的格式 (該格式將會拒絕⾮標準交易)。 ▷池中或位於主分⽀區塊中的⼀個匹配交易必須是存在的。 ▷對於每⼀個輸⼊,如果引⽤的輸出存在於池中任何的交易,該交易將被拒絕。 ▷對於每⼀個輸⼊,在主分⽀和交易池中尋找引⽤的輸出交易。如果輸出交易缺少任何⼀個輸⼊,該交易將成為⼀個孤⽴的交易。如果與其匹配的交易還沒有出現在池中,那麼將被加⼊到孤⽴交易池中。 ▷對於每⼀個輸⼊,如果引⽤的輸出交易是⼀個coinbase輸出,該輸⼊必須⾄少獲得 COINBASE_MATURITY (100)個確認。對於每⼀個輸⼊,引⽤的輸出是必須存在的,並且沒有被花費。使⽤引⽤的輸出交易獲得輸⼊值,並檢查每⼀個輸⼊值和總值是否在規定值的範圍內 (⼩於2100萬個幣,⼤於0)。如果輸⼊值的總和⼩於輸出值的總和,交易將被中⽌。 ▷如果交易費⽤太低以⾄於⽆法進⼊⼀個空的區塊,交易將被拒絕。 ▷每⼀個輸⼊的解鎖指令碼必須依據相應輸出的鎖定指令碼來驗證。

  2. 通過完成⼯作量證明演算法的驗算。

    1. **判斷區塊高度是否為2016的倍數,因為每2016個區塊會調整難度。**如果沒達到2016的倍數,只需要驗證目標難度是否相等。(比特幣10分鐘產生一個區塊,2016差不多需要2周時間

    2. 如果需要調整,則需要計算前2016個區塊的實際產生時間。

      1. 往前遍歷2016個區塊,找到前2016個區塊的開始區塊

      2. 計算前2016個區塊產生的實際時間:當前已確認的區塊時間 - 前2016個區塊的開始區塊的時間

      3. 限制實際時間在調整係數(4)之內

        //獲取前2016個區塊的實際使用時間
        int timespan = (int) (prev.getTimeSeconds() - blockIntervalAgo.getTimeSeconds());
        // Limit the adjustment step.
        final int targetTimespan = this.getTargetTimespan();
        if (timespan < targetTimespan / 4)
            timespan = targetTimespan / 4;
        if (timespan > targetTimespan * 4)
            timespan = targetTimespan * 4;
        
      4. 根據前2016個區塊的實際使用時間計算新的目標值(難度的另一種表示方式)

        newTarget = timespan * preDifficulty/targetTimespan

        targetTimespan = 14 * 24 * 60 * 60 兩個星期

        目標值壓縮為4個位元組的表達方式。可以節約儲存。

        //難度太大會限制為創世塊的預設難度(大約10分鐘一個區塊)
        if (newTarget.compareTo(this.getMaxTarget()) > 0) {
            log.info("Difficulty hit proof of work limit: {}", newTarget.toString(16));
            newTarget = this.getMaxTarget();
        }
        
      5. 比較計算的難度與區塊中的目標難度是否相等。因為驗證的過程跟礦工挖礦的過程是一樣流程。

  3. 每個節點對區塊鏈進⾏獨⽴選擇,在⼯作量證明機制下選擇累計⼯作量最⼤的區塊鏈

    1. 如果該區塊是接著主鏈打的,則處理交易。(處理時會驗證交易,驗證過程就是第一步)將交易輸入引用的UTXO刪掉;交易的輸出轉換為UTXO儲存。

    2. 如果該區塊不是接著主鏈打的,會判斷區塊的工作量是否大於了主鏈的,如果沒有大於,則只是將區塊連結上。

      什麼叫工作量呢?

      工作量被定義為:平均情況下生成一個合法區塊所需的嘗試次數(nonce)

        /**
           * Returns the work represented by this block.<p>
           *
           * Work is defined as the number of tries needed to solve a block in the
           * average case. Consider a difficulty target that covers 5% of all possible
           * hash values. Then the work of the block will be 20. As the target gets
           * lower, the amount of work goes up.
           */
          public BigInteger getWork() throws VerificationException {
            //將目標難度值轉換為256bit的大數,這個數值表示該難度下的目標hash值
            //(假設這個hash值覆蓋了所有hash範圍的5%,則工作量為20)
              BigInteger target = getDifficultyTargetAsInteger();
            //LARGEST_HASH: 該數字比最大可表示的SHA-256雜湊大1
              return LARGEST_HASH.divide(target.add(BigInteger.ONE));
          }
      

      鏈累積的工作量計算:

        /**
        * 構建區塊儲存的索引。包含了這條鏈從創創世塊到現在的所有工作量
        */
          public StoredBlock build(Block block) throws VerificationException {
              //僅代表了該鏈的工作量總和(這裡的this指前一個區塊)每個區塊都如此計算,就是一個累和
              BigInteger chainWork = this.chainWork.add(block.getWork());
              int height = this.height + 1;
              return new StoredBlock(block, chainWork, height);
          }
      

      如何判斷是否產生新的主鏈?

      假設現在產生了分叉,則只需要計算出分叉鏈的鏈累積工作量,與當前主鏈的對比,如果大於,則新產生的鏈為主鏈,舊的主鏈則會作廢。

      其實就是最長鏈原則,因為鏈越長,累積的工作量會越大。

        //判斷當前鏈是否比主鏈的工作量大,如果大於,則為新主鏈
          public boolean moreWorkThan(StoredBlock other) {
              return chainWork.compareTo(other.chainWork) > 0;
          }
      
    3. 比特幣處理分叉

  4. 上圖中,區塊高度為103的區塊到來後,新的主鏈產生。這時候,會用當前主鏈的102,與新主鏈的103向前遍歷,找到分叉點101區塊。(注意:)

  5. 通過最長鏈原則,以6個區塊為確認主鏈,確認主鏈後,分叉鏈需要回退交易。花掉的utxo進行增加,而新增的utxo進行刪除。

    注意:6個區塊:判斷當前區塊時間是否大於等於最近11個區塊的區塊時間的中位數(6),所以只能回退最近6個區塊內的資料,反向意思為:6個之前的無法回退,即6個區塊確認主鏈。

  6. 如果區塊分叉,但還沒有成為最長鏈,只是儲存了區塊,並沒有花費UTXO(因為可能已經花費過了)

對於新區塊102,不會處理交易。因為主鏈已經處理過了102區塊的交易(可能區塊中的交易不完全一樣,但也沒有關係,因為可能新區塊102中的新交易已經被主鏈的103區塊打包並處理了)

⽐特幣將區塊間隔設計為10分鐘,是在更快速的交易確認和更低的分叉概率間作出的妥協。更短的區塊產⽣間隔會讓交易清算更快地完成,也會導致更加頻繁地區塊鏈分叉。

什麼叫51%攻擊?

大家可能也經常聽到,比特幣51%的概念。看上面分叉圖。如果有人掌握了全網51%的算力,則就可以比任何人都先計算出合法nonce,所以就可以連續接著102的新區塊打區塊,當下面這條鏈的長度長於主鏈,那就是一條新的主鏈。畢竟你這麼厲害,都控制了51%的算力,那我們就跟著你這條鏈玩吧,你就是主鏈。

分叉的總類?

  • 軟分叉:上面說的情況都是軟分叉的情況。因為可能有多個礦工在同一時間找到合法的nonce,加上網路原因等。
  • 硬分叉:指強制升級,可能重大bug被發現,也或者被黑客攻擊,都可以使用硬分叉來修復。但需要全網很多礦工都同步升級。

什麼叫孤兒區塊?

比如現在來了一個105高度的區塊,使用此區塊的preBlockHash無法找到父區塊,則此區塊就稱為孤兒區塊。

當收到一個區塊為孤兒區塊後,會將其存入孤兒區塊池中。並嘗試遍歷孤兒區塊池中的區塊,看是否能連結到主鏈上。因為處於分散式網路中,每個節點的網路情況、同步等有差異,可能節點會先收到高度更高的區塊,然後再收到高度低一點的區塊,然後再與本地的鏈高度連結上。

為什麼coinbase中的UTXO需要100個確認才能使用呢?

因為如果使用了coinbase中的UTXO的交易被打包進入了兩個分叉的區塊中。上面說過,重新切換主鏈時,舊主鏈的交易需要回退,產生的UTXO需要刪除,使用了的UTXO需要新增。所以,導致新主鏈中引用了coinbase中UTXO的交易無效,因為已經沒有這個UTXO了。這樣就會導致區塊驗證不通過,主鏈無法重新切換。所以比特幣假設沒有不可能有這麼長遠的分叉,6個確認主鏈已經小於100個確認coinbase。

總結

此篇略說了一些比特幣相關的技術知識,以及比特幣的一些原理。文中有錯誤之處,敬請各位指出。也歡迎大家評論區一起交流學習。

參考資料

《精通比特幣》強烈推薦。

bitcoinJ原始碼

本文使用 mdnice 排版

相關文章