成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

基于Java語言構(gòu)建區(qū)塊鏈(五)—— 地址(錢包)

entner / 3877人閱讀

摘要:本質(zhì)上,比特幣錢包就是一對這樣的密鑰。例如,一個(gè)錯(cuò)誤比特幣地址就不會被錢包認(rèn)為是有效的地址,否則這種錯(cuò)誤會造成資金的丟失。

最終內(nèi)容請以原文為準(zhǔn):https://wangwei.one/posts/f90...
引言

在 上一篇 文章當(dāng)中,我們開始了交易機(jī)制的實(shí)現(xiàn)。你已經(jīng)了解到交易的一些非個(gè)人特征:沒有用戶賬戶,您的個(gè)人數(shù)據(jù)(例如:姓名、護(hù)照號碼以及SSN(美國社會安全卡(Social Security Card)上的9 位數(shù)字))不是必需的,并且不存儲在比特幣的任何地方。但仍然必須有一些東西能夠識別你是這些交易輸出的所有者(例如:鎖定在這些輸出上的幣的所有者)。這就是比特幣地址的作用所在。到目前為止,我們只是使用了任意的用戶定義的字符串當(dāng)做地址,現(xiàn)在是時(shí)候來實(shí)現(xiàn)真正的地址了,就像它們在比特幣中實(shí)現(xiàn)的一樣。

比特幣地址

這里有一個(gè)比特幣地址的示例:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。這是一個(gè)非常早期的比特幣地址,據(jù)稱是屬于中本聰?shù)谋忍貛诺刂?。比特幣地址是公開的。如果你想要給某人發(fā)送比特幣,你需要知道對方的比特幣地址。但是地址(盡管它是唯一的)并不能作為你是一個(gè)錢包所有者的憑證。事實(shí)上,這樣的地址是公鑰的一種可讀性更好的表示 。在比特幣中,你的身份是存儲在你計(jì)算機(jī)上(或存儲在你有權(quán)訪問的其他位置)的一對(或多對)私鑰和公鑰。比特幣依靠加密算法的組合來創(chuàng)建這些密鑰,并保證世界上沒有其他人任何人可以在沒有物理訪問密鑰的情況下訪問您的比特幣。

比特幣地址與公鑰不同。比特幣地址是由公鑰經(jīng)過單向的哈希函數(shù)生成的

接下來,讓我們來討論一下這些加密算法。

注意:不要向本篇文章中的代碼所生成的任何比特幣地址發(fā)送真實(shí)的比特幣來進(jìn)行測試,否則后果自負(fù)……
公鑰密碼學(xué)

公鑰加密算法(public-key cryptography)使用的是密鑰對:公鑰和私鑰。公鑰屬于非敏感信息,可以向任何人透露。相比之下,私鑰不能公開披露:除了所有者之外,任何人都不能擁有私鑰的權(quán)限,因?yàn)樗怯米魉姓邩?biāo)識的私鑰。你的私鑰代表就是你(當(dāng)然是在加密貨幣世界里的)。

本質(zhì)上,比特幣錢包就是一對這樣的密鑰。當(dāng)你安裝一個(gè)錢包應(yīng)用程序或者使用比特幣客戶端去生成一個(gè)新的地址時(shí),它們就為你創(chuàng)建好了一個(gè)密鑰對。在比特幣種,誰控制了私鑰,誰就掌握了所有發(fā)往對應(yīng)公鑰地址上所有比特幣的控制權(quán)。

私鑰和公鑰只是隨機(jī)的字節(jié)序列,因此它們不能被打印在屏幕上供人讀取。這就是為什么比特幣會用一種算法將公鑰的字節(jié)序列轉(zhuǎn)化為人類可讀的字符串形式。

如果你曾今使用過比特幣錢包的應(yīng)用程序,它可能會為你生成助記詞密碼短語。這些助記詞可以用來替代私鑰,并且能夠生成私鑰。這種機(jī)制是通過 BIP-039 來實(shí)現(xiàn)的。

好了,現(xiàn)在我們已經(jīng)知道在比特幣中由什么來決定用戶的標(biāo)識了。但是,比特幣是如何校驗(yàn)交易輸出(和它里面存儲的一些幣)的所有權(quán)的呢?

數(shù)字簽名

在數(shù)學(xué)和密碼學(xué)中,有個(gè)數(shù)字簽名的概念,這套算法保證了以下幾點(diǎn):

保證數(shù)據(jù)從發(fā)送端傳遞到接收端的過程中不會被篡改;

數(shù)據(jù)由某個(gè)發(fā)送者創(chuàng)建;

發(fā)送者不能否認(rèn)發(fā)送的數(shù)據(jù);

通過對數(shù)據(jù)應(yīng)用簽名算法(即簽署數(shù)據(jù)),可以得到一個(gè)簽名,以后可以對其進(jìn)行驗(yàn)證。數(shù)字簽名需要使用私鑰,而驗(yàn)證則需要公鑰。

為了能夠簽署數(shù)據(jù)我們需要:

用于被簽名的數(shù)據(jù);

私鑰。

簽名操作會產(chǎn)生一個(gè)存儲在交易輸入中的簽名。為了能夠驗(yàn)證一個(gè)簽名,我們需要:

簽名之后的數(shù)據(jù);

簽名;

公鑰。

簡單來講,這個(gè)驗(yàn)證的過程可以被描述為:檢查簽名是由被簽名數(shù)據(jù)加上私鑰得來,并且這個(gè)公鑰也是由該私鑰生成。

數(shù)字簽名并不是一種加密方法,你無法從簽名反向構(gòu)造出源數(shù)據(jù)。這個(gè)和我們 前面 提到過的Hash算法有點(diǎn)類似:通過對一個(gè)數(shù)據(jù)使用Hash算法,你可以得到該數(shù)據(jù)的唯一表示。它們兩者的不同之處在于,簽名算法多了一個(gè)密鑰對:它讓數(shù)字簽名得以驗(yàn)證成為可能。

但是密鑰對也能夠用于去加密數(shù)據(jù):私鑰用于加密數(shù)據(jù),公鑰用于解密數(shù)據(jù)。不過比特幣并沒有使用加密算法。

在比特幣中,每一筆交易輸入都會被該筆交易的創(chuàng)建者進(jìn)行簽名。比特幣中的每一筆交易在放入?yún)^(qū)塊之前都必須得到驗(yàn)證。驗(yàn)證的意思就是:

檢查交易輸入是否擁有引用前一筆交易中交易輸出的權(quán)限

檢查交易的簽名是否正確

數(shù)據(jù)簽名以及簽名驗(yàn)證的過程如下圖所示:

讓我們來回顧一下交易的完整生命周期:

最開始,會有一個(gè)包含了Coinbase交易的創(chuàng)世區(qū)塊。由于在Coinbase交易中沒有真正的交易輸入,所以它不需要簽名。Coinbase交易的交易輸出會包含一個(gè)Hashing之后的公鑰(使用的算法為 RIPEMD16(SHA256(PubKey))

當(dāng)一個(gè)人發(fā)送比特幣時(shí),會創(chuàng)建一筆交易。這筆交易的交易輸入會引用前一筆或多筆交易的交易輸出。每一個(gè)交易輸入將會存儲未經(jīng)Hashing處理的公鑰以及整個(gè)交易的簽名信息。

當(dāng)比特幣網(wǎng)絡(luò)中的其他節(jié)點(diǎn)收到其他節(jié)點(diǎn)廣播的交易數(shù)據(jù)之后將,將會對其進(jìn)行驗(yàn)證。其他的事情除外,他們將會驗(yàn)證:

檢查交易輸入中公鑰的Hash值是否與它所引用的交易輸出的Hash值想匹配,這是確保發(fā)送方只能發(fā)送屬于他們自己的比特幣。

檢查簽名是否正確,這是為了確保這筆交易是由比特幣的真正所有者創(chuàng)建的。

當(dāng)一個(gè)礦工準(zhǔn)備開始開采一個(gè)新的區(qū)塊時(shí),他會將交易信息放入?yún)^(qū)塊中,然后開始挖礦。

當(dāng)一個(gè)區(qū)塊完成挖礦之后,網(wǎng)絡(luò)中的其他節(jié)點(diǎn)將會收到一條區(qū)塊已挖礦完畢的消息,并且他們會把這個(gè)區(qū)塊添加到區(qū)塊鏈中去。

當(dāng)一個(gè)區(qū)塊被添加到區(qū)塊鏈之后,就標(biāo)志著這筆交易已經(jīng)完成,它所產(chǎn)生的交易輸出將會在新的交易中被引用。

橢圓曲線密碼學(xué)

正如前面所提到的那樣,公鑰和私鑰是一串隨機(jī)的字符序列。由于私鑰是用來識別比特幣所有者身份的緣故,因此有一個(gè)必要的條件:這個(gè)隨機(jī)算法必須產(chǎn)生真正的隨機(jī)序列。我們不希望意外地生成其他人所擁有的私鑰。也就是要保證隨機(jī)序列的絕對唯一性。

比特幣是使用的橢圓曲線來生成的私鑰。橢圓曲線是一個(gè)非常復(fù)雜的數(shù)學(xué)概念,這里我們不做詳細(xì)的介紹(如果你對此非常好奇,可以點(diǎn)擊 this gentle introduction to elliptic curves 進(jìn)行詳細(xì)的 了解,警告:數(shù)學(xué)公式)。我們需要知道的是,這些曲線可以用來生成真正大而隨機(jī)的數(shù)字。比特幣所采用的曲線算法能夠隨機(jī)生成一個(gè)介于0到 2^2^56之間的數(shù)字(這是一個(gè)非常大的數(shù)字,用十進(jìn)制表示的話,大約是10^77, 而整個(gè)可見的宇宙中,原子數(shù)在 10^78 到 10^82 之間) 。這么巨大的上限意味著產(chǎn)生兩個(gè)一樣的私鑰是幾乎不可能的事情。

另外,我們將會使用比特幣中所使用的 ECDSA (橢圓曲線數(shù)字簽名算法)去簽署交易信息。

Base58和Base58Check編碼

現(xiàn)在讓我們回到上面提到的比特幣地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa . 現(xiàn)在我們知道這個(gè)地址其實(shí)是公鑰的一種可讀高的表示方式。如果我們對他進(jìn)行解碼,我們會看到公鑰看起來是這樣子的(字節(jié)序列的十六進(jìn)制的表示方式):

0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93
Base58

Base64使用了26個(gè)小寫字母、26個(gè)大寫字母、10個(gè)數(shù)字以及兩個(gè)符號(例如“+”和“/”),用于在電子郵件這樣的基于文本的媒介中傳輸二進(jìn)制數(shù)據(jù)。Base64通常用于編碼郵件中的附件。Base58是一種基于文本的二進(jìn)制編碼格式,用在比特幣和其它的加密貨幣中。這種編碼格式不僅實(shí)現(xiàn)了數(shù)據(jù)壓縮,保持了易讀性,還具有錯(cuò)誤診斷功能。Base58是Base64編碼格式的子集,同樣使用大小寫字母和10個(gè)數(shù)字,但舍棄了一些容易錯(cuò)讀和在特定字體中容易混淆的字符。具體地,Base58不含Base64中的0(數(shù)字0)、O(大寫字母o)、l(小寫字母L)、I(大寫字母i),以及“+”和“/”兩個(gè)字符。簡而言之,Base58就是由不包括(0,O,l,I)的大小寫字母和數(shù)字組成。

比特幣的Base58字母表:

123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
Base58Check

Base58Check是一種常用在比特幣中的Base58編碼格式,增加了錯(cuò)誤校驗(yàn)碼來檢查數(shù)據(jù)在轉(zhuǎn)錄中出現(xiàn)的錯(cuò)誤。校驗(yàn)碼長4個(gè)字節(jié),添加到需要編碼的數(shù)據(jù)之后。校驗(yàn)碼是從需要編碼的數(shù)據(jù)的哈希值中得到的,所以可以用來檢測并避免轉(zhuǎn)錄和輸入中產(chǎn)生的錯(cuò)誤。使用Base58check編碼格式時(shí),編碼軟件會計(jì)算原始數(shù)據(jù)的校驗(yàn)碼并和結(jié)果數(shù)據(jù)中自帶的校驗(yàn)碼進(jìn)行對比。二者不匹配則表明有錯(cuò)誤產(chǎn)生,那么這個(gè)Base58Check格式的數(shù)據(jù)就是無效的。例如,一個(gè)錯(cuò)誤比特幣地址就不會被錢包認(rèn)為是有效的地址,否則這種錯(cuò)誤會造成資金的丟失。

為了使用Base58Check編碼格式對數(shù)據(jù)(數(shù)字)進(jìn)行編碼,首先我們要對數(shù)據(jù)添加一個(gè)稱作“版本字節(jié)”的前綴,這個(gè)前綴用來明確需要編碼的數(shù)據(jù)的類型。例如,比特幣地址的前綴是0(十六進(jìn)制是0x00),而對私鑰編碼時(shí)前綴是128(十六進(jìn)制是0x80)。

讓我們以示意圖的形式展示一下從公鑰得到地址的過程:

因此,上述解碼的公鑰由三部分組成:

Version  Public key hash                           Checksum
00       62E907B15CBF27D5425399EBF6F0FB50EBB88F18  C29B7D93

由于哈希函數(shù)是單向的(也就說無法逆轉(zhuǎn)回去),所以不可能從一個(gè)哈希中提取公鑰。不過通過執(zhí)行哈希函數(shù)并進(jìn)行哈希比較,我們可以檢查一個(gè)公鑰是否被用于哈希的生成。

OK,現(xiàn)在我們有了所有的東西,讓我們來編寫一些代碼。 當(dāng)一些概念被寫成代碼時(shí),我們會對此理解的更加清晰和深刻。

地址實(shí)現(xiàn)

讓我們從 Wallet 的構(gòu)成開始,這里我們需要先引入一個(gè)maven包:


    org.bouncycastle
    bcprov-jdk15on
    1.59

錢包結(jié)構(gòu)

/**
 * 錢包
 *
 * @author wangwei
 * @date 2018/03/14
 */
@Data
@AllArgsConstructor
public class Wallet {

    // 校驗(yàn)碼長度
    private static final int ADDRESS_CHECKSUM_LEN = 4;
    /**
     * 私鑰
     */
    private BCECPrivateKey privateKey;
    /**
     * 公鑰
     */
    private byte[] publicKey;

    public Wallet() {
        initWallet();
    }

    /**
     * 初始化錢包
     */
    private void initWallet() {
        try {
            KeyPair keyPair = newECKeyPair();
            BCECPrivateKey privateKey = (BCECPrivateKey) keyPair.getPrivate();
            BCECPublicKey publicKey = (BCECPublicKey) keyPair.getPublic();

            byte[] publicKeyBytes = publicKey.getQ().getEncoded(false);

            this.setPrivateKey(privateKey);
            this.setPublicKey(publicKeyBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 創(chuàng)建新的密鑰對
     *
     * @return
     * @throws Exception
     */
    private KeyPair newKeyPair() throws Exception {
        // 注冊 BC Provider
        Security.addProvider(new BouncyCastleProvider());
        // 創(chuàng)建橢圓曲線算法的密鑰對生成器,算法為 ECDSA
        KeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);
        // 橢圓曲線(EC)域參數(shù)設(shè)定
        // bitcoin 為什么會選擇 secp256k1,詳見:https://bitcointalk.org/index.php?topic=151120.0
        ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
        g.initialize(ecSpec, new SecureRandom());
        return g.generateKeyPair();
    }
 
}    

所謂的錢包,其實(shí)本質(zhì)上就是一個(gè)密鑰對。這里我們需要借助 KeyPairGenerator 生成密鑰對。

接著,我們來生成比特幣的錢包地址:

public class Wallet {
    
    ...
   
    /**
     * 獲取錢包地址
     *
     * @return
     */
    public String getAddress() throws Exception {
        // 1. 獲取 ripemdHashedKey
        byte[] ripemdHashedKey = BtcAddressUtils.ripeMD160Hash(this.getPublicKey().getEncoded());

        // 2. 添加版本 0x00
        ByteArrayOutputStream addrStream = new ByteArrayOutputStream();
        addrStream.write((byte) 0);
        addrStream.write(ripemdHashedKey);
        byte[] versionedPayload = addrStream.toByteArray();

        // 3. 計(jì)算校驗(yàn)碼
        byte[] checksum = BtcAddressUtils.checksum(versionedPayload);

        // 4. 得到 version + paylod + checksum 的組合
        addrStream.write(checksum);
        byte[] binaryAddress = addrStream.toByteArray();

        // 5. 執(zhí)行Base58轉(zhuǎn)換處理
        return Base58Check.rawBytesToBase58(binaryAddress);
    }
    
    ...
}

這個(gè)時(shí)候,你就可以得到 真實(shí)的比特幣地址 了,并且你可以到 blockchain.info 上去檢查這個(gè)地址的余額。

例如,通過 getAddress 方法,得到了一個(gè)比特幣地址為:1rZ9SjXMRwnbW3Pu8itC1HtNBVHERSQhaACbL16

我敢保證,無論你生成多少次比特幣地址,它的余額始終為0.這就是為什么選擇適當(dāng)?shù)墓€密碼算法如此重要:考慮到私鑰是隨機(jī)數(shù)字,產(chǎn)生相同數(shù)字的機(jī)會必須盡可能低。 理想情況下,它必須低至“永不”。

另外,需要注意的是你不需要連接到比特幣的節(jié)點(diǎn)上去獲取比特幣的地址。有關(guān)地址生成的開源算法工具包已經(jīng)有很多編程語言和庫實(shí)現(xiàn)了。

現(xiàn)在,我們需要去修改交易輸入與輸出,讓他們開始使用真實(shí)的地址:

交易輸入
/**
 * 交易輸入
 *
 * @author wangwei
 * @date 2017/03/04
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TXInput {

    /**
     * 交易Id的hash值
     */
    private byte[] txId;
    /**
     * 交易輸出索引
     */
    private int txOutputIndex;
    /**
     * 簽名
     */
    private byte[] signature;
    /**
     * 公鑰
     */
    private byte[] pubKey;


    /**
     * 檢查公鑰hash是否用于交易輸入
     *
     * @param pubKeyHash
     * @return
     */
    public boolean usesKey(byte[] pubKeyHash) {
        byte[] lockingHash = BtcAddressUtils.ripeMD160Hash(this.getPubKey());
        return Arrays.equals(lockingHash, pubKeyHash);
    }

}
交易輸出
/**
 * 交易輸出
 *
 * @author wangwei
 * @date 2017/03/04
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TXOutput {

    /**
     * 數(shù)值
     */
    private int value;
    /**
     * 公鑰Hash
     */
    private byte[] pubKeyHash;

    /**
     * 創(chuàng)建交易輸出
     *
     * @param value
     * @param address
     * @return
     */
    public static TXOutput newTXOutput(int value, String address) {
        // 反向轉(zhuǎn)化為 byte 數(shù)組
        byte[] versionedPayload = Base58Check.base58ToBytes(address);
        byte[] pubKeyHash = Arrays.copyOfRange(versionedPayload, 1, versionedPayload.length);
        return new TXOutput(value, pubKeyHash);
    }

    /**
     * 檢查交易輸出是否能夠使用指定的公鑰
     *
     * @param pubKeyHash
     * @return
     */
    public boolean isLockedWithKey(byte[] pubKeyHash) {
        return Arrays.equals(this.getPubKeyHash(), pubKeyHash);
    }

}
代碼中還有很多其他的地方需要變動,這里不一一指出,詳見文末的源碼連接。

注意,由于我們不會去實(shí)現(xiàn)腳本語言特性,所以我們不再使用 scriptPubKeyscriptSig 字段。取而代之的是,我們將 scriptSig 拆分為了 signaturepubKey 字段,scriptPubKey 重命名為了 pubKeyHash 。我們將會實(shí)現(xiàn)類似于比特幣中的交易輸出鎖定/解鎖邏輯和交易輸入的簽名邏輯,但是我們會在方法中執(zhí)行此操作。

usesKey 用于檢查交易輸入中的公鑰是否能夠解鎖交易輸出。需要注意的是,交易輸入中存儲的是未經(jīng)hash過的公鑰,但是方法實(shí)現(xiàn)中對它做了一步 ripeMD160Hash 轉(zhuǎn)化。

isLockedWithKey 用于檢查提供的公鑰Hash是否能夠用于解鎖交易輸出,這個(gè)方法是 usesKey 的補(bǔ)充。usesKey 被用于 getAllSpentTXOs 方法中,isLockedWithKey 被用于 findUnspentTransactions 方法中,這樣使得在前后兩筆交易之間建立起了連接。

newTXOutput 方法中,將 value 鎖定到了 address 上。當(dāng)我們向別人發(fā)送比特幣時(shí),我們只知道他們的地址,因此函數(shù)將地址作為唯一的參數(shù)。然后解碼地址,并從中提取公鑰哈希并保存在PubKeyHash字段中。

現(xiàn)在,讓我們一起來檢查一下是否能夠正常運(yùn)行:

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet
wallet address : 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet
wallet address : 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet
wallet address : 19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1

$ java -jar blockchain-java-jar-with-dependencies.jar  createblockchain -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh

Elapsed Time: 6.77 seconds 
correct hash Hex: 00000e44be0c94c39a4fef24c67d85c428e8bfbd227e292d75c0f4d398e2e81c 

Done ! 

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh
Balance of "13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh": 10

$ java -jar blockchain-java-jar-with-dependencies.jar  send -from 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e -to  13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVd -amount 5
java.lang.Exception: ERROR: Not enough funds

$ java -jar blockchain-java-jar-with-dependencies.jar  send -from 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh -to 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e-amount 5
Elapsed Time: 4.477 seconds 
correct hash Hex: 00000da41dfacc8032a553ed5b1aa5e24318d5d89ca14a16c4f70129609c8365 

Success!

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh
Balance of "13dJAkeMyjjXvWCmhsXpDqnszHvhFSLVdh": 5

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e
Balance of "1BCY5gCXUMiFYc5ieBMfEUaZn3GYkvVZ2e": 5

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1
Balance of "19aomsC58CQ1tPzNLx7kV9yjk1pqZtSzL1": 0

Nice! 現(xiàn)在讓我們一起來實(shí)現(xiàn)交易簽名部分的內(nèi)容。

簽名實(shí)現(xiàn)

交易數(shù)據(jù)必須被簽名,因?yàn)檫@是比特幣中能夠保證不能花費(fèi)屬于他人比特幣的唯一方法。如果一個(gè)簽名是無效的,那么這筆交易也是無效的,這樣的話,這筆交易就不能被添加到區(qū)塊鏈中去。

我們已經(jīng)有了實(shí)現(xiàn)交易簽名的所有片段,還有一個(gè)事情除外:用于簽名的數(shù)據(jù)。交易數(shù)據(jù)中哪一部分是真正用于簽名的呢?難道是全部數(shù)據(jù)?選擇用于簽名的數(shù)據(jù)相當(dāng)?shù)闹匾?。用于簽名的?shù)據(jù)必須包含以獨(dú)特且唯一的方式標(biāo)識數(shù)據(jù)的信息。例如,僅對交易輸出簽名是沒有意義的,因?yàn)榇撕灻粫紤]發(fā)送發(fā)與接收方。

考慮到交易數(shù)據(jù)要解鎖前面的交易輸出,重新分配交易輸出中的 value 值,并且鎖定新的交易輸出,因此下面這些數(shù)據(jù)是必須被簽名的:

存儲在解鎖了的交易輸出中的公鑰Hash。它標(biāo)識了交易的發(fā)送方。

存儲在新的、鎖定的交易輸出中的公鑰Hash。它標(biāo)識了交易的接收方。

新的交易輸出中包含的 value 值。

在比特幣中,鎖定/解鎖邏輯存儲在腳本中,解鎖腳本存儲在交易輸入的 ScriptSig 字段中,而鎖定腳本存儲在交易輸出的 ScriptPubKey 的字段中。 由于比特幣允許不同類型的腳本,因此它會對ScriptPubKey的全部內(nèi)容進(jìn)行簽名。

如你所見,我們不需要去對存儲在交易輸入中的公鑰進(jìn)行簽名。正因?yàn)槿绱耍诒忍貛胖?,所簽名的并不是一個(gè)交易,而是一個(gè)去除部分內(nèi)容的交易輸入副本,交易輸入里面存儲了被引用交易輸出的 ScriptPubKey 。

獲取修剪后的交易副本的詳細(xì)過程在這里. 雖然它可能已經(jīng)過時(shí)了,但是我并沒有找到另一個(gè)更可靠的來源。

OK,它看起來有點(diǎn)復(fù)雜,因此讓我們來開始coding吧。我們將從 Sign 方法開始:

public class Transaction {

   ...

   /**
     * 簽名
     *
     * @param privateKey 私鑰
     * @param prevTxMap  前面多筆交易集合
     */
    public void sign(BCECPrivateKey privateKey, Map prevTxMap) throws Exception {
        // coinbase 交易信息不需要簽名,因?yàn)樗淮嬖诮灰纵斎胄畔?        if (this.isCoinbase()) {
            return;
        }
        // 再次驗(yàn)證一下交易信息中的交易輸入是否正確,也就是能否查找對應(yīng)的交易數(shù)據(jù)
        for (TXInput txInput : this.getInputs()) {
            if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) {
                throw new Exception("ERROR: Previous transaction is not correct");
            }
        }

        // 創(chuàng)建用于簽名的交易信息的副本
        Transaction txCopy = this.trimmedCopy();
      
        Security.addProvider(new BouncyCastleProvider());
        Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
        ecdsaSign.initSign(privateKey);

        for (int i = 0; i < txCopy.getInputs().length; i++) {
            TXInput txInputCopy = txCopy.getInputs()[i];
            // 獲取交易輸入TxID對應(yīng)的交易數(shù)據(jù)
            Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId()));
            // 獲取交易輸入所對應(yīng)的上一筆交易中的交易輸出
            TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()];
            txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
            txInputCopy.setSignature(null);
            // 得到要簽名的數(shù)據(jù),即交易ID
            txCopy.setTxId(txCopy.hash());
            txInputCopy.setPubKey(null);

            // 對整個(gè)交易信息僅進(jìn)行簽名,即對交易ID進(jìn)行簽名
            ecdsaSign.update(txCopy.getTxId());
            byte[] signature = ecdsaSign.sign();

            // 將整個(gè)交易數(shù)據(jù)的簽名賦值給交易輸入,因?yàn)榻灰纵斎胄枰麄€(gè)交易信息的簽名
            // 注意是將得到的簽名賦值給原交易信息中的交易輸入
            this.getInputs()[i].setSignature(signature);
        }
    }

    ...
    
}    

這個(gè)方法需要私鑰和前面多筆交易集合作為參數(shù)。正如前面所提到的那樣,為了能夠?qū)灰仔畔⑦M(jìn)行簽名,我們需要能夠訪問到被交易數(shù)據(jù)中的交易輸入所引用的交易輸出,因此我們需要得到存儲這些交易輸出的交易信息。

讓我們來一步一步review這個(gè)方法:

if (this.isCoinbase()) {
   return;
}

由于 coinbase 交易信息不存在交易輸入信息,因此它不需要簽名,直接return.

Transaction txCopy = this.trimmedCopy();

創(chuàng)建交易的副本

public class Transaction {

   ...   
   
   /**
     * 創(chuàng)建用于簽名的交易數(shù)據(jù)副本
     *
     * @return
     */
    public Transaction trimmedCopy() {
        TXInput[] tmpTXInputs = new TXInput[this.getInputs().length];
        for (int i = 0; i < this.getInputs().length; i++) {
            TXInput txInput = this.getInputs()[i];
            tmpTXInputs[i] = new TXInput(txInput.getTxId(), txInput.getTxOutputIndex(), null, null);
        }

        TXOutput[] tmpTXOutputs = new TXOutput[this.getOutputs().length];
        for (int i = 0; i < this.getOutputs().length; i++) {
            TXOutput txOutput = this.getOutputs()[i];
            tmpTXOutputs[i] = new TXOutput(txOutput.getValue(), txOutput.getPubKeyHash());
        }

        return new Transaction(this.getTxId(), tmpTXInputs, tmpTXOutputs);
    }
    
    ...
    
}    

這個(gè)交易數(shù)據(jù)的副本包含了交易輸入與交易輸出,但是交易輸入的 SignaturePubKey 需要設(shè)置為null。

使用私鑰初始化 SHA256withECDSA 簽名算法:

Security.addProvider(new BouncyCastleProvider());
        Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
        ecdsaSign.initSign(privateKey);

接下來,我們迭代交易副本中的交易輸入:

for (TXInput txInput : txCopy.getInputs()) {
      // 獲取交易輸入TxID對應(yīng)的交易數(shù)據(jù)
      Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInputCopy.getTxId()));
      // 獲取交易輸入所對應(yīng)的上一筆交易中的交易輸出
      TXOutput prevTxOutput = prevTx.getOutputs()[txInputCopy.getTxOutputIndex()];
      txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
      txInputCopy.setSignature(null);

在每一個(gè) txInput中,signature 都需要設(shè)置為null(僅僅是為了二次確認(rèn)檢查),并且 pubKey 設(shè)置為它所引用的交易輸出的 pubKeyHash 字段。在此刻,除了當(dāng)前的正在循環(huán)的交易輸入(txInput)外,其他所有的交易輸入都是"空的",也就是說他們的 SignaturePubKey 字段被設(shè)置為 null。因此,交易輸入是被分開簽名的,盡管這對于我們的應(yīng)用并不十分緊要,但是比特幣允許交易包含引用了不同地址的輸入。

Hash 方法對交易進(jìn)行序列化,并使用 SHA-256 算法進(jìn)行哈希。哈希后的結(jié)果就是我們要簽名的數(shù)據(jù)。在獲取完哈希,我們應(yīng)該重置 PubKey 字段,以便于它不會影響后面的迭代。

// 得到要簽名的數(shù)據(jù),即交易ID
txCopy.setTxId(txCopy.hash());
txInput.setPubKey(null);

現(xiàn)在,最關(guān)鍵的部分來了:

// 對整個(gè)交易信息僅進(jìn)行簽名,即對交易ID進(jìn)行簽名
Security.addProvider(new BouncyCastleProvider());
Signature ecdsaSign = Signature.getInstance("SHA256withECDSA",BouncyCastleProvider.PROVIDER_NAME);
ecdsaSign.initSign(privateKey);
ecdsaSign.update(txCopy.getTxId());
byte[] signature = ecdsaSign.sign();

// 將整個(gè)交易數(shù)據(jù)的簽名賦值給交易輸入,因?yàn)榻灰纵斎胄枰麄€(gè)交易信息的簽名
// 注意是將得到的簽名賦值給原交易信息中的交易輸入
this.getInputs()[i].setSignature(signature);

使用 SHA256withECDSA 簽名算法加上私鑰,來對交易ID進(jìn)行簽名,從而得到了交易輸入所要設(shè)置的交易簽名。

現(xiàn)在,讓我們來實(shí)現(xiàn)交易的驗(yàn)證功能:

public class Transaction {

    ...

    /**
     * 驗(yàn)證交易信息
     *
     * @param prevTxMap 前面多筆交易集合
     * @return
     */
    public boolean verify(Map prevTxMap) throws Exception {
        // coinbase 交易信息不需要簽名,也就無需驗(yàn)證
        if (this.isCoinbase()) {
            return true;
        }

        // 再次驗(yàn)證一下交易信息中的交易輸入是否正確,也就是能否查找對應(yīng)的交易數(shù)據(jù)
        for (TXInput txInput : this.getInputs()) {
            if (prevTxMap.get(Hex.encodeHexString(txInput.getTxId())) == null) {
                throw new Exception("ERROR: Previous transaction is not correct");
            }
        }

        // 創(chuàng)建用于簽名驗(yàn)證的交易信息的副本
        Transaction txCopy = this.trimmedCopy();
        
        Security.addProvider(new BouncyCastleProvider());
        ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1");
        KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);
        Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
        
        for (int i = 0; i < this.getInputs().length; i++) {
            TXInput txInput = this.getInputs()[i];
            // 獲取交易輸入TxID對應(yīng)的交易數(shù)據(jù)
            Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId()));
            // 獲取交易輸入所對應(yīng)的上一筆交易中的交易輸出
            TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()];

            TXInput txInputCopy = txCopy.getInputs()[i];
            txInputCopy.setSignature(null);
            txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
            // 得到要簽名的數(shù)據(jù),即交易ID
            txCopy.setTxId(txCopy.hash());
            txInputCopy.setPubKey(null);
            
            // 使用橢圓曲線 x,y 點(diǎn)去生成公鑰Key
            BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33));
            BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65));
            ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y);

            ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters);
            PublicKey publicKey = keyFactory.generatePublic(keySpec);
            ecdsaVerify.initVerify(publicKey);
            ecdsaVerify.update(txCopy.getTxId());
            if (!ecdsaVerify.verify(txInput.getSignature())) {
                return false;
            }
        }
        return true;
    }
    
    ...
}

首選,同前面簽名一樣,我們先獲取交易的拷貝數(shù)據(jù):

Transaction txCopy = this.trimmedCopy();

獲取橢圓曲線參數(shù)和簽名類:

Security.addProvider(new BouncyCastleProvider());
        ECParameterSpec ecParameters = ECNamedCurveTable.getParameterSpec("secp256k1");
        KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);
        Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);

接下來,我們來檢查每一個(gè)交易輸入的簽名是否正確:

for (int i = 0; i < this.getInputs().length; i++) {
    TXInput txInput = this.getInputs()[i];
    // 獲取交易輸入TxID對應(yīng)的交易數(shù)據(jù)
    Transaction prevTx = prevTxMap.get(Hex.encodeHexString(txInput.getTxId()));
    // 獲取交易輸入所對應(yīng)的上一筆交易中的交易輸出
    TXOutput prevTxOutput = prevTx.getOutputs()[txInput.getTxOutputIndex()];

    TXInput txInputCopy = txCopy.getInputs()[i];
    txInputCopy.setSignature(null);
    txInputCopy.setPubKey(prevTxOutput.getPubKeyHash());
    // 得到要簽名的數(shù)據(jù),即交易ID
    txCopy.setTxId(txCopy.hash());
    txInputCopy.setPubKey(null);
}    

這部分與Sign方法中的相同,因?yàn)樵隍?yàn)證過程中我們需要簽署相同的數(shù)據(jù)。

// 使用橢圓曲線 x,y 點(diǎn)去生成公鑰Key
BigInteger x = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 1, 33));
BigInteger y = new BigInteger(1, Arrays.copyOfRange(txInput.getPubKey(), 33, 65));
ECPoint ecPoint = ecParameters.getCurve().createPoint(x, y);

ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, ecParameters);
PublicKey publicKey = keyFactory.generatePublic(keySpec);
ecdsaVerify.initVerify(publicKey);
ecdsaVerify.update(txCopy.getTxId());
if (!ecdsaVerify.verify(txInput.getSignature())) {
    return false;
}

由于交易輸入中存儲的 pubkey ,實(shí)際上是橢圓曲線上的一對 x,y 坐標(biāo),所以我們可以從 pubKey 得到公鑰PublicKey,然后在用公鑰去簽名進(jìn)行驗(yàn)證。如果驗(yàn)證成功,則返回true,否則,返回false。

現(xiàn)在,我們需要一個(gè)方法來獲取以前的交易。 由于這需要與區(qū)塊鏈互動,我們將使其成為 blockchain 的一種方法:

public class Blockchain {

    ...

    /**
     * 依據(jù)交易ID查詢交易信息
     *
     * @param txId 交易ID
     * @return
     */
    private Transaction findTransaction(byte[] txId) throws Exception {
        for (BlockchainIterator iterator = this.getBlockchainIterator(); iterator.hashNext(); ) {
            Block block = iterator.next();
            for (Transaction tx : block.getTransactions()) {
                if (Arrays.equals(tx.getTxId(), txId)) {
                    return tx;
                }
            }
        }
        throw new Exception("ERROR: Can not found tx by txId ! ");
    }


    /**
     * 進(jìn)行交易簽名
     *
     * @param tx         交易數(shù)據(jù)
     * @param privateKey 私鑰
     */
    public void signTransaction(Transaction tx, BCECPrivateKey privateKey) throws Exception {
        // 先來找到這筆新的交易中,交易輸入所引用的前面的多筆交易的數(shù)據(jù)
        Map prevTxMap = new HashMap<>();
        for (TXInput txInput : tx.getInputs()) {
            Transaction prevTx = this.findTransaction(txInput.getTxId());
            prevTxMap.put(Hex.encodeHexString(txInput.getTxId()), prevTx);
        }
        tx.sign(privateKey, prevTxMap);
    }

    /**
     * 交易簽名驗(yàn)證
     *
     * @param tx
     */
    private boolean verifyTransactions(Transaction tx) throws Exception {
        Map prevTx = new HashMap<>();
        for (TXInput txInput : tx.getInputs()) {
            Transaction transaction = this.findTransaction(txInput.getTxId());
            prevTx.put(Hex.encodeHexString(txInput.getTxId()), transaction);
        }
        return tx.verify(prevTx);
    }

}

現(xiàn)在,我們需要對我們的交易進(jìn)行真正的簽名和驗(yàn)證了,交易的簽名發(fā)生在 newUTXOTransaction 中:

 public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception {
        
    ...

    Transaction newTx = new Transaction(null, txInputs, txOutput);
    newTx.setTxId(newTx.hash());

    // 進(jìn)行交易簽名
    blockchain.signTransaction(newTx, senderWallet.getPrivateKey());

    return newTx;
}

交易的驗(yàn)證發(fā)生在一筆交易被放入?yún)^(qū)塊之前:

public void mineBlock(Transaction[] transactions) throws Exception {
    // 挖礦前,先驗(yàn)證交易記錄
    for (Transaction tx : transactions) {
        if (!this.verifyTransactions(tx)) {
           throw new Exception("ERROR: Fail to mine block ! Invalid transaction ! ");
        }
    }

    ...
}

OK,讓我們再一次對整個(gè)工程的代碼做一個(gè)測試,測試結(jié)果:

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet
wallet address : 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet
wallet address : 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB

$ java -jar blockchain-java-jar-with-dependencies.jar  createwallet
wallet address : 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f

$ java -jar blockchain-java-jar-with-dependencies.jar  createblockchain -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6

Elapsed Time: 164.961 seconds 
correct hash Hex: 00000524231ae1832c49957848d2d1871cc35ff4d113c23be1937c6dff5cdf2a 

Done ! 

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6
Balance of "1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6": 10

$ java -jar blockchain-java-jar-with-dependencies.jar  send -from 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB -to  13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f -amount 5
java.lang.Exception: ERROR: Not enough funds

$ java -jar blockchain-java-jar-with-dependencies.jar  send -from 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6 -to 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB -amount 5
Elapsed Time: 54.92 seconds 
correct hash Hex: 00000354f86cde369d4c39d2b3016ac9a74956425f1348b4c26b2cddb98c100b 

Success!


$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6
Balance of "1GTh9Yjh4eH2a69FMX2kvSpnkJAgLdXFD6": 5

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB
Balance of "1NnmFCuNnhPZHfXu38wZi8uEb446pDhaGB": 5

$ java -jar blockchain-java-jar-with-dependencies.jar  getbalance -address 13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f
Balance of "13K6rfHPifjdH4HXN2okpo4uxNRfVCx13f": 0

Good!沒有任何錯(cuò)誤!

讓我們注釋掉 NewUTXOTransaction 方法中的一行代碼,確保未被簽名的交易不能被添加到區(qū)塊中:

blockchain.signTransaction(newTx, senderWallet.getPrivateKey());

測試結(jié)果:

java.lang.Exception: Fail to verify transaction ! transaction invalid ! 
    at one.wangwei.blockchain.block.Blockchain.verifyTransactions(Blockchain.java:334)
    at one.wangwei.blockchain.block.Blockchain.mineBlock(Blockchain.java:76)
    at one.wangwei.blockchain.cli.CLI.send(CLI.java:202)
    at one.wangwei.blockchain.cli.CLI.parse(CLI.java:79)
    at one.wangwei.blockchain.BlockchainTest.main(BlockchainTest.java:23)
總結(jié)

這一節(jié),我們學(xué)到了:

使用橢圓曲線加密算法,如何去創(chuàng)建錢包;

了解到了如何去生成比特幣地址;

如何去對交易信息進(jìn)行簽名并對簽名進(jìn)行驗(yàn)證;

到目前為止,我們已經(jīng)實(shí)現(xiàn)了比特幣的許多關(guān)鍵特性! 我們已經(jīng)實(shí)現(xiàn)了除外網(wǎng)絡(luò)外的幾乎所有功能,并且在下一篇文章中,我們將繼續(xù)完善交易這一環(huán)節(jié)機(jī)制。

資料

源代碼:https://github.com/wangweiX/b...

Elliptic Curve Key Pair Generation and Key Factories

How to create public key objects with x and y coordinates?

Public-key cryptography

Digital signatures

Elliptic curve

Elliptic curve cryptography

ECDSA

Technical background of Bitcoin addresses

Address

Base58

A gentle introduction to elliptic curve cryptography

?

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/24011.html

相關(guān)文章

  • 區(qū)塊開發(fā)中使用的最流行的編程語言

    摘要:我們目前正處于一個(gè)新興的區(qū)塊鏈開發(fā)行業(yè)中。,一種在以太坊開發(fā)人員中流行的新的簡單編程語言,因?yàn)樗怯糜陂_發(fā)以太坊智能合約的語言。它是全球至少萬開發(fā)人員使用的世界上最流行的編程語言之一。以太坊,主要是針對工程師使用進(jìn)行區(qū)塊鏈以太坊開發(fā)的詳解。 我們目前正處于一個(gè)新興的區(qū)塊鏈開發(fā)行業(yè)中。區(qū)塊鏈技術(shù)處于初期階段,然而這種顛覆性技術(shù)已經(jīng)成功地風(fēng)靡全球,并且最近經(jīng)歷了一場與眾不同的繁榮。由于許多...

    2shou 評論0 收藏0
  • 基于Java語言構(gòu)建區(qū)塊(四)—— 交易(UTXO)

    摘要:交易這一環(huán)節(jié)是整個(gè)比特幣系統(tǒng)當(dāng)中最為關(guān)鍵的一環(huán),并且區(qū)塊鏈唯一的目的就是通過安全的可信的方式來存儲交易信息,防止它們創(chuàng)建之后被人惡意篡改。在比特幣中,交易輸出先于交易輸入出現(xiàn)。 showImg(https://segmentfault.com/img/remote/1460000013923562?w=3200&h=1400); 最終內(nèi)容請以原文為準(zhǔn): https://wangwei....

    Vultr 評論0 收藏0
  • 基于Java語言構(gòu)建區(qū)塊(四)—— 交易(UTXO)

    摘要:交易這一環(huán)節(jié)是整個(gè)比特幣系統(tǒng)當(dāng)中最為關(guān)鍵的一環(huán),并且區(qū)塊鏈唯一的目的就是通過安全的可信的方式來存儲交易信息,防止它們創(chuàng)建之后被人惡意篡改。在比特幣中,交易輸出先于交易輸入出現(xiàn)。 showImg(https://segmentfault.com/img/remote/1460000013923562?w=3200&h=1400); 最終內(nèi)容請以原文為準(zhǔn): https://wangwei....

    ctriptech 評論0 收藏0
  • 如何開發(fā)比特幣錢包應(yīng)用程序

    摘要:創(chuàng)建比特幣錢包需要一組優(yōu)秀的程序員。如何使用流行的庫構(gòu)建自己的比特幣錢包應(yīng)用程序創(chuàng)建比特幣錢包應(yīng)用程序的一種方法是依賴現(xiàn)有工具。具有以下功能它允許開發(fā)人員使用密碼加密創(chuàng)建比特幣錢包應(yīng)用程序。 盡管目前加密貨幣市場相當(dāng)黯淡,但比特幣和其他山寨幣繼續(xù)受歡迎。每天都有新的交易者加入市場,希望能夠在下一個(gè)價(jià)格高漲時(shí)獲利。 隨著市場的突飛猛進(jìn),開發(fā)商也在獲益。新交易者的首要任務(wù)是設(shè)置比特幣錢包。...

    Cympros 評論0 收藏0
  • 如何開發(fā)比特幣錢包應(yīng)用程序

    摘要:創(chuàng)建比特幣錢包需要一組優(yōu)秀的程序員。如何使用流行的庫構(gòu)建自己的比特幣錢包應(yīng)用程序創(chuàng)建比特幣錢包應(yīng)用程序的一種方法是依賴現(xiàn)有工具。具有以下功能它允許開發(fā)人員使用密碼加密創(chuàng)建比特幣錢包應(yīng)用程序。 盡管目前加密貨幣市場相當(dāng)黯淡,但比特幣和其他山寨幣繼續(xù)受歡迎。每天都有新的交易者加入市場,希望能夠在下一個(gè)價(jià)格高漲時(shí)獲利。 隨著市場的突飛猛進(jìn),開發(fā)商也在獲益。新交易者的首要任務(wù)是設(shè)置比特幣錢包。...

    crossea 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<