翻译—区块链(二):用Java创建你的第一个区块链—交易

(PS:part3 2月份儿后就见不到影子了…)

第二篇文章我们将做以下:

· 创建一个简单的钱包

· 使用我们的区块链发送已签名的交易

· 感受更加酷的东西

以上将会产生我们自己的加密货币(sorta)

继续我们上篇的,我们已经有了基本的可校验的区块链。但是我们目前的链仅仅存储了相当无用的信息。今天我们打算用交易代替这些无用信息(我们的区块可以容纳多种交易),允许我们创建一个非常简单的加密货币。我们把它取一个新货币的名字"Noob"币。

· Dependencies:你需要导入包bounceycastle

准备一个钱包

在加密货币中,硬币的所有权被转移到了区块链上用来交易,参与者有一个可以进行虚拟货币交易的网址。在他们的基本形式中,钱包仅仅能存储这些网址,但大多数钱包也是一种软件,能够在区块链上做新的交易。

image.png

然后让我们创建一个Wallet类来支持我们的公钥和私钥:

package noobchain;
import java.security.*;

public class Wallet {
    public PrivateKey privateKey;
    public PublicKey publicKey;
}

用公钥和私钥来做什么呢?

对于我们的"Noob"币来说,公钥将作为我们的地址。在与其他人交易的时候分享公钥是可以的。我们的私钥被用来签名我们的交易,来保证没有人可以用他们的私钥来花费我们的"Noob"币。用户必须保存他们自己的私钥!我们还将公钥和交易一起发送,他可以用来验证我们的签名是否有效、数据是否被修改。

image.png

我们使用KeyPair类来生成我们的公钥和私钥。使用Elliptic-curve cryptography来生成KeyPairs。让我们在Wallet类中添加generateKeyPair()方法并在构造器中调用它。

package noobchain;
import java.security.*;

public class Wallet {
    
    public PrivateKey privateKey;
    public PublicKey publicKey;
    
    public Wallet(){
        generateKeyPair();  
    }
        
    public void generateKeyPair() {
        try {
            KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ECDSA","BC");
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime192v1");
            // Initialize the key generator and generate a KeyPair
            keyGen.initialize(ecSpec, random);   //256 bytes provides an acceptable security level
                KeyPair keyPair = keyGen.generateKeyPair();
                // Set the public and private keys from the keyPair
                privateKey = keyPair.getPrivate();
                publicKey = keyPair.getPublic();
        }catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
    
}

现在我们的Wallet有一个轮廓了,让我们在交易中看看。

交易 & 签名

每个交易都携带以下数据:

· 发送者基金的公钥地址

· 接受者基金的公钥地址

· 要交易的价值/基金数量

· 输入:是交易之前的参考(证明发送方有基金去交易)

· 输出:表明相关地址接受到的交易量/价值。(这些输出会在一个新的交易中被作为输入)

· 一个加密的签名,证明地址的所有者发送此交易只有一次,数据没有修改过(例如:防止第三方修改交易的数量)

接下来创建Transaction类:

import java.security.*;
import java.util.ArrayList;

public class Transaction {
    
    public String transactionId; // this is also the hash of the transaction.
    public PublicKey sender; // senders address/public key.
    public PublicKey reciepient; // Recipients address/public key.
    public float value;
    public byte[] signature; // this is to prevent anybody else from spending funds in our wallet.
    
    public ArrayList<TransactionInput> inputs = new ArrayList<TransactionInput>();
    public ArrayList<TransactionOutput> outputs = new ArrayList<TransactionOutput>();
    
    private static int sequence = 0; // a rough count of how many transactions have been generated. 
    
    // Constructor: 
    public Transaction(PublicKey from, PublicKey to, float value,  ArrayList<TransactionInput> inputs) {
        this.sender = from;
        this.reciepient = to;
        this.value = value;
        this.inputs = inputs;
    }
    
    // This Calculates the transaction hash (which will be used as its Id)
    private String calulateHash() {
        sequence++; //increase the sequence to avoid 2 identical transactions having the same hash
        return StringUtil.applySha256(
                StringUtil.getStringFromKey(sender) +
                StringUtil.getStringFromKey(reciepient) +
                Float.toString(value) + sequence
                );
    }
}

我们还应该有空的TransactionInput类和TransactionOutput类。不用担心我们将在之后补充它们。

我们的Transaction类还包含了相关的生成/验证签名的方法和验证交易的方法。

但是……?

签名的目的是什么?它怎样工作的?

签名有两个非常重要的任务在我们的区块链中:第一,它只允许所有者消费他们的虚拟币,第二,他们在新的区块被挖掘出之前防止其他人篡改已经提交的交易。(在切入点?at the point of entry)。

例如:Bob想要发送2个币给Sally,然后他们的钱包软件会生成这个交易,把它提交给矿工并传入到下一个区块中。矿工试图把2个币的收件人改成John。然而,幸运的是,Bob已经用他的私钥签名了交易数据,允许任何人验证交易数据是否被Bob用他的公钥修改了。(因为没有其他人的公钥就可以验证交易)。

我们可以从之前的Block类中看到,我们签名是一串字节数组,所以,让我们创建一个生成他们的方法。首先,我们需要一些辅助功能StringUtil类。


//Applies ECDSA Signature and returns the result ( as bytes ).
        public static byte[] applyECDSASig(PrivateKey privateKey, String input) {
        Signature dsa;
        byte[] output = new byte[0];
        try {
            dsa = Signature.getInstance("ECDSA", "BC");
            dsa.initSign(privateKey);
            byte[] strByte = input.getBytes();
            dsa.update(strByte);
            byte[] realSig = dsa.sign();
            output = realSig;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return output;
    }
    
    //Verifies a String signature 
    public static boolean verifyECDSASig(PublicKey publicKey, String data, byte[] signature) {
        try {
            Signature ecdsaVerify = Signature.getInstance("ECDSA", "BC");
            ecdsaVerify.initVerify(publicKey);
            ecdsaVerify.update(data.getBytes());
            return ecdsaVerify.verify(signature);
        }catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String getStringFromKey(Key key) {
        return Base64.getEncoder().encodeToString(key.getEncoded());
    }

现在,让我们利用这些签名方法在Transaction类中,添加generateSianature()和verifySignature()方法。

当一个新当交易被添加到区块中到时候,签名将会由矿工去验证。

测试 Wallets 和 Signatures

现在我们几乎做了一半让我们测试一些东西。在NoobChain类中添加一些新的变量和替换我们的main方法:

import java.security.Security;
import java.util.ArrayList;
import java.util.Base64;
import com.google.gson.GsonBuilder;

public class NoobChain {
    
    public static ArrayList<Block> blockchain = new ArrayList<Block>();
    public static int difficulty = 5;
    public static Wallet walletA;
    public static Wallet walletB;

    public static void main(String[] args) {    
        //Setup Bouncey castle as a Security Provider
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); 
        //Create the new wallets
        walletA = new Wallet();
        walletB = new Wallet();
        //Test public and private keys
        System.out.println("Private and public keys:");
        System.out.println(StringUtil.getStringFromKey(walletA.privateKey));
        System.out.println(StringUtil.getStringFromKey(walletA.publicKey));
        //Create a test transaction from WalletA to walletB 
        Transaction transaction = new Transaction(walletA.publicKey, walletB.publicKey, 5, null);
        transaction.generateSignature(walletA.privateKey);
        //Verify the signature works and verify it from the public key
        System.out.println("Is signature verified");
        System.out.println(transaction.verifiySignature());
        
    }

我们创建了两个wallets类walletA和walletB,打印walletA的私钥和公钥。生成一个交易并使用它的私钥进行签名。

你的输出应该是以下结果:

image.png

(Time to pat your self on the back.)现在我们仅仅需要创建/验证输出和输入,然后把交易存储在区块链中。

如何拥有加密货币…?

为了拥有1个比特币,你必须收到1个比特币。账簿不是真的给你添加了1个比特币并且给发送者减少了1个比特币,然后交易输出1个比特币被发送到你的地址了。(交易本次输入参考于上一个交易的输出)。

你的钱包余额是发给你的所有未消费的交易输出的总和。

从这一点开始,我们将遵循比特币约定并调用未消费的交易输出:UTXO。

然后我们创建TransactionInput类:

public class TransactionInput {
    public String transactionOutputId; //Reference to TransactionOutputs -> transactionId
    public TransactionOutput UTXO; //Contains the Unspent transaction output
    
    public TransactionInput(String transactionOutputId) {
        this.transactionOutputId = transactionOutputId;
    }
}

TransactionOutput类:

import java.security.PublicKey;

public class TransactionOutput {
    public String id;
    public PublicKey reciepient; //also known as the new owner of these coins.
    public float value; //the amount of coins they own
    public String parentTransactionId; //the id of the transaction this output was created in
    
    //Constructor
    public TransactionOutput(PublicKey reciepient, float value, String parentTransactionId) {
        this.reciepient = reciepient;
        this.value = value;
        this.parentTransactionId = parentTransactionId;
        this.id = StringUtil.applySha256(StringUtil.getStringFromKey(reciepient)+Float.toString(value)+parentTransactionId);
    }
    
    //Check if coin belongs to you
    public boolean isMine(PublicKey publicKey) {
        return (publicKey == reciepient);
    }
    
}

交易输出将显示发送给每一方后的最终金额。当在新交易中作为输入饮用时,将会作为你要发送硬币的证明。

输入 & 输出2:进行交易…

区块链可能接受许多交易并且这条链可能是非常非常长的。它可能需要很长的时间来处理新的交易,因为我们必须找到并检查它的输入。为了解决这个问题,我们额外收集所有可用作输入的为消费的交易。在NoobChain类中添加如下UTXO集合:

public class NoobChain {
    
    public static ArrayList<Block> blockchain = new ArrayList<Block>();
    public static HashMap<String,TransactionOutputs> UTXOs = new HashMap<String,TransactionOutputs>(); //list of all unspent transactions. 
    public static int difficulty = 5;
    public static Wallet walletA;
    public static Wallet walletB;

    public static void main(String[] args) {    

是时候深入了解细节了…

好的,是时候让我们把所有东西放在一起来处理交易了,在我们的Transaction类中使用boolean类型的processTransaction方法来处理细节:

//Returns true if new transaction could be created. 
public boolean processTransaction() {
        
        if(verifiySignature() == false) {
            System.out.println("#Transaction Signature failed to verify");
            return false;
        }
                
        //gather transaction inputs (Make sure they are unspent):
        for(TransactionInput i : inputs) {
            i.UTXO = NoobChain.UTXOs.get(i.transactionOutputId);
        }

        //check if transaction is valid:
        if(getInputsValue() < NoobChain.minimumTransaction) {
            System.out.println("#Transaction Inputs to small: " + getInputsValue());
            return false;
        }
        
        //generate transaction outputs:
        float leftOver = getInputsValue() - value; //get value of inputs then the left over change:
        transactionId = calulateHash();
        outputs.add(new TransactionOutput( this.reciepient, value,transactionId)); //send value to recipient
        outputs.add(new TransactionOutput( this.sender, leftOver,transactionId)); //send the left over 'change' back to sender      
                
        //add outputs to Unspent list
        for(TransactionOutput o : outputs) {
            NoobChain.UTXOs.put(o.id , o);
        }
        
        //remove transaction inputs from UTXO lists as spent:
        for(TransactionInput i : inputs) {
            if(i.UTXO == null) continue; //if Transaction can't be found skip it 
            NoobChain.UTXOs.remove(i.UTXO.id);
        }
        
        return true;
    }
    
//returns sum of inputs(UTXOs) values
    public float getInputsValue() {
        float total = 0;
        for(TransactionInput i : inputs) {
            if(i.UTXO == null) continue; //if Transaction can't be found skip it 
            total += i.UTXO.value;
        }
        return total;
    }

//returns sum of outputs:
    public float getOutputsValue() {
        float total = 0;
        for(TransactionOutput o : outputs) {
            total += o.value;
        }
        return total;
    }

使用这个方法,我们执行一些检查以确保交易有效,然后收集输入并生成输出(查看代码中的注释行以获得更多信息)。

重要的一步,最后,我们从UTXO列表中丢弃输入,意味着交易输出只能用作输入一次。因此必须使用输入的完整值,所以发送者发送"change"反馈给自己。

image.png

最后我们修改以下Wallet类:

· 收集我们的余额(通过循环UTXO列表检查交易输出isMine)

· 生成我们的交易…

import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class Wallet {
    
    public PrivateKey privateKey;
    public PublicKey publicKey;
    
    public HashMap<String,TransactionOutput> UTXOs = new HashMap<String,TransactionOutput>(); //only UTXOs owned by this wallet.
    
    public Wallet() {...
        
    public void generateKeyPair() {...
    
  //returns balance and stores the UTXO's owned by this wallet in this.UTXOs
    public float getBalance() {
        float total = 0;    
        for (Map.Entry<String, TransactionOutput> item: NoobChain.UTXOs.entrySet()){
            TransactionOutput UTXO = item.getValue();
            if(UTXO.isMine(publicKey)) { //if output belongs to me ( if coins belong to me )
                UTXOs.put(UTXO.id,UTXO); //add it to our list of unspent transactions.
                total += UTXO.value ; 
            }
        }  
        return total;
    }
    //Generates and returns a new transaction from this wallet.
    public Transaction sendFunds(PublicKey _recipient,float value ) {
        if(getBalance() < value) { //gather balance and check funds.
            System.out.println("#Not Enough funds to send transaction. Transaction Discarded.");
            return null;
        }
    //create array list of inputs
        ArrayList<TransactionInput> inputs = new ArrayList<TransactionInput>();
    
        float total = 0;
        for (Map.Entry<String, TransactionOutput> item: UTXOs.entrySet()){
            TransactionOutput UTXO = item.getValue();
            total += UTXO.value;
            inputs.add(new TransactionInput(UTXO.id));
            if(total > value) break;
        }
        
        Transaction newTransaction = new Transaction(publicKey, _recipient , value, inputs);
        newTransaction.generateSignature(privateKey);
        
        for(TransactionInput input: inputs){
            UTXOs.remove(input.transactionOutputId);
        }
        return newTransaction;
    }
    
}

给我们的区块添加交易

现在我们有一个可用的交易系统了,我们需要把它结合在我们的区块链中。我们应用一个交易的集合替换掉无用的数据。但是在单个区块中可能有1000个交易,我们的hash计算包含的内容太多了…不过不用担心,我们可以使用交易的merkleroot。

让我们在StringUtil类中添加一个helper方法去生成merkleroot。

//Tacks in array of transactions and returns a merkle root.
public static String getMerkleRoot(ArrayList<Transaction> transactions) {
        int count = transactions.size();
        ArrayList<String> previousTreeLayer = new ArrayList<String>();
        for(Transaction transaction : transactions) {
            previousTreeLayer.add(transaction.transactionId);
        }
        ArrayList<String> treeLayer = previousTreeLayer;
        while(count > 1) {
            treeLayer = new ArrayList<String>();
            for(int i=1; i < previousTreeLayer.size(); i++) {
                treeLayer.add(applySha256(previousTreeLayer.get(i-1) + previousTreeLayer.get(i)));
            }
            count = treeLayer.size();
            previousTreeLayer = treeLayer;
        }
        String merkleRoot = (treeLayer.size() == 1) ? treeLayer.get(0) : "";
        return merkleRoot;
    }

然后在Block类中做一些修改(merkroot的修改):

import java.util.ArrayList;
import java.util.Date;

public class Block {
    
    public String hash;
    public String previousHash; 
    public String merkleRoot;
    public ArrayList<Transaction> transactions = new ArrayList<Transaction>(); //our data will be a simple message.
    public long timeStamp; //as number of milliseconds since 1/1/1970.
    public int nonce;
    
    //Block Constructor.  
    public Block(String previousHash ) {
        this.previousHash = previousHash;
        this.timeStamp = new Date().getTime();
        
        this.hash = calculateHash(); //Making sure we do this after we set the other values.
    }
    
    //Calculate new hash based on blocks contents
    public String calculateHash() {
        String calculatedhash = StringUtil.applySha256( 
                previousHash +
                Long.toString(timeStamp) +
                Integer.toString(nonce) + 
                merkleRoot
                );
        return calculatedhash;
    }
    
    //Increases nonce value until hash target is reached.
    public void mineBlock(int difficulty) {
        merkleRoot = StringUtil.getMerkleRoot(transactions);
        String target = StringUtil.getDificultyString(difficulty); //Create a string with difficulty * "0" 
        while(!hash.substring( 0, difficulty).equals(target)) {
            nonce ++;
            hash = calculateHash();
        }
        System.out.println("Block Mined!!! : " + hash);
    }
    
    //Add transactions to this block
    public boolean addTransaction(Transaction transaction) {
        //process transaction and check if valid, unless block is genesis block then ignore.
        if(transaction == null) return false;       
        if((previousHash != "0")) {
            if((transaction.processTransaction() != true)) {
                System.out.println("Transaction failed to process. Discarded.");
                return false;
            }
        }
        transactions.add(transaction);
        System.out.println("Transaction Successfully added to Block");
        return true;
    }
    
}

注意我们也修改了Block类构造器,因为我们不再需要String类型的data,并在计算hash的方法中添加上merkleroot。

我们有一个boolean的addTransaction方法添加交易并返回这个交易是否添加成功。

最后一步

我们应该测试一下用钱包发送币,并修改我们区块链的校验方法。首先我们需要一个方法把新币引入进来。有很多办法去创造新币,例如比特币中:矿工把交易包含在内作为每个开采新区块的奖励。

就目前而言,我们将在第一个区块(创世块)中释放我们所希望拥有的所有货币。就像比特币一样,我们会对创世块进行硬编码。

修改我们的NoobChain类为他所需要的:

· 一个创世块释放了100个Noob币给walletA

· 考虑到交易的账户修改链的校验

· 用一些交易测试来看看每个东西是不是在正常运行

public class NoobChain {
    
    public static ArrayList<Block> blockchain = new ArrayList<Block>();
    public static HashMap<String,TransactionOutput> UTXOs = new HashMap<String,TransactionOutput>();
    
    public static int difficulty = 3;
    public static float minimumTransaction = 0.1f;
    public static Wallet walletA;
    public static Wallet walletB;
    public static Transaction genesisTransaction;

    public static void main(String[] args) {    
        //add our blocks to the blockchain ArrayList:
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); //Setup Bouncey castle as a Security Provider
        
        //Create wallets:
        walletA = new Wallet();
        walletB = new Wallet();     
        Wallet coinbase = new Wallet();
        
        //create genesis transaction, which sends 100 NoobCoin to walletA: 
        genesisTransaction = new Transaction(coinbase.publicKey, walletA.publicKey, 100f, null);
        genesisTransaction.generateSignature(coinbase.privateKey);   //manually sign the genesis transaction    
        genesisTransaction.transactionId = "0"; //manually set the transaction id
        genesisTransaction.outputs.add(new TransactionOutput(genesisTransaction.reciepient, genesisTransaction.value, genesisTransaction.transactionId)); //manually add the Transactions Output
        UTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0)); //its important to store our first transaction in the UTXOs list.
        
        System.out.println("Creating and Mining Genesis block... ");
        Block genesis = new Block("0");
        genesis.addTransaction(genesisTransaction);
        addBlock(genesis);
        
        //testing
        Block block1 = new Block(genesis.hash);
        System.out.println("\nWalletA's balance is: " + walletA.getBalance());
        System.out.println("\nWalletA is Attempting to send funds (40) to WalletB...");
        block1.addTransaction(walletA.sendFunds(walletB.publicKey, 40f));
        addBlock(block1);
        System.out.println("\nWalletA's balance is: " + walletA.getBalance());
        System.out.println("WalletB's balance is: " + walletB.getBalance());
        
        Block block2 = new Block(block1.hash);
        System.out.println("\nWalletA Attempting to send more funds (1000) than it has...");
        block2.addTransaction(walletA.sendFunds(walletB.publicKey, 1000f));
        addBlock(block2);
        System.out.println("\nWalletA's balance is: " + walletA.getBalance());
        System.out.println("WalletB's balance is: " + walletB.getBalance());
        
        Block block3 = new Block(block2.hash);
        System.out.println("\nWalletB is Attempting to send funds (20) to WalletA...");
        block3.addTransaction(walletB.sendFunds( walletA.publicKey, 20));
        System.out.println("\nWalletA's balance is: " + walletA.getBalance());
        System.out.println("WalletB's balance is: " + walletB.getBalance());
        
        isChainValid();
        
    }
    
    public static Boolean isChainValid() {
        Block currentBlock; 
        Block previousBlock;
        String hashTarget = new String(new char[difficulty]).replace('\0', '0');
        HashMap<String,TransactionOutput> tempUTXOs = new HashMap<String,TransactionOutput>(); //a temporary working list of unspent transactions at a given block state.
        tempUTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0));
        
        //loop through blockchain to check hashes:
        for(int i=1; i < blockchain.size(); i++) {
            
            currentBlock = blockchain.get(i);
            previousBlock = blockchain.get(i-1);
            //compare registered hash and calculated hash:
            if(!currentBlock.hash.equals(currentBlock.calculateHash()) ){
                System.out.println("#Current Hashes not equal");
                return false;
            }
            //compare previous hash and registered previous hash
            if(!previousBlock.hash.equals(currentBlock.previousHash) ) {
                System.out.println("#Previous Hashes not equal");
                return false;
            }
            //check if hash is solved
            if(!currentBlock.hash.substring( 0, difficulty).equals(hashTarget)) {
                System.out.println("#This block hasn't been mined");
                return false;
            }
            
            //loop thru blockchains transactions:
            TransactionOutput tempOutput;
            for(int t=0; t <currentBlock.transactions.size(); t++) {
                Transaction currentTransaction = currentBlock.transactions.get(t);
                
                if(!currentTransaction.verifiySignature()) {
                    System.out.println("#Signature on Transaction(" + t + ") is Invalid");
                    return false; 
                }
                if(currentTransaction.getInputsValue() != currentTransaction.getOutputsValue()) {
                    System.out.println("#Inputs are note equal to outputs on Transaction(" + t + ")");
                    return false; 
                }
                
                for(TransactionInput input: currentTransaction.inputs) {    
                    tempOutput = tempUTXOs.get(input.transactionOutputId);
                    
                    if(tempOutput == null) {
                        System.out.println("#Referenced input on Transaction(" + t + ") is Missing");
                        return false;
                    }
                    
                    if(input.UTXO.value != tempOutput.value) {
                        System.out.println("#Referenced input Transaction(" + t + ") value is Invalid");
                        return false;
                    }
                    
                    tempUTXOs.remove(input.transactionOutputId);
                }
                
                for(TransactionOutput output: currentTransaction.outputs) {
                    tempUTXOs.put(output.id, output);
                }
                
                if( currentTransaction.outputs.get(0).reciepient != currentTransaction.reciepient) {
                    System.out.println("#Transaction(" + t + ") output reciepient is not who it should be");
                    return false;
                }
                if( currentTransaction.outputs.get(1).reciepient != currentTransaction.sender) {
                    System.out.println("#Transaction(" + t + ") output 'change' is not sender.");
                    return false;
                }
                
            }
            
        }
        System.out.println("Blockchain is valid");
        return true;
    }
    
    public static void addBlock(Block newBlock) {
        newBlock.mineBlock(difficulty);
        blockchain.add(newBlock);
    }
}

输出为以下这样:

image.png

在你的区块链中钱包现在可以安全的发送货币了,只要他们有资金发送即可。这意味着你拥有资金的本地加密货币了。

恭喜你完成了区块链上的交易

你已经成功的创建了自己的加密货币(有点!sort of!),你的区块链现在的状态是:

· 允许用户用new wallet()创建钱包。

· 提供有用Elliptic-Curve加密的公钥和私钥的钱包。

· 通过使用数字签名算法证明所有权,确认资金转移。

· 允许用户在你的区块链上使用交易:Block.addTransaction(walletA.sendFunds( walletB.publicKey, 20))


你可以在这里下载作者的源码

原文链接:https://medium.com/programmers-blockchain/creating-your-first-blockchain-with-java-part-2-transactions-2cdac335e0ce

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容