The target de esta serie de tutoriales, es ayudarte a construir una imagen de cómo se podría desarrollar la tecnología de blockchain. Puedes encontrar la parte 1 aquí: Construyendo tu propio Blockchain en JavaScript – Parte 1.
In this second tutorial we are going to ..:
- Create a simple wallet.
- Send signed transactions using our blockchain.
All of the above will result in our own crypto currency.
Continuing with the last tutorial, we have a verifiable basic Blockchain. But currently our chain only stores quite useless messages. Today we are going to replace this data with transactions ("Our block may carry out multiple transactions"), which will allow us to create a crypto-currency very simple. We will call our new currency "NoobCoin".
This tutorial assumes that you have followed the other tutorial.
Dependencies: You will need to import bounceycastle and GSON.
Prepare a wallet
In crypto-currencies, the ownership of the currency is transferred on the blockchain as transactions, the participants have an address to which the funds can be sent. In their basic form wallets can simply store these addresses, however most wallets are also software able to carry out new transactions in the Blockchain.
So let's create a class of wallet to keep our public and private key:
package noobchain; import java.security.*; public class Wallet { public PrivateKey privateKey; public PublicKey publicKey; }
What are public and private keys for?
For our 'noobcoin' the public key will act as our address. It is okay to share this public key with others to get paid. Our private key is used to sign our transactions, so that no one can spend our noobcoins rather than the owner of the private key. Users will have to keep their private key secret. We also send our public key along with the transaction and it can be used to verify that our signature is valid and that the data has not been altered.
We generate our private and public keys in a KeyPair. We will use elliptic curve cryptography to generate our KeyPairs. Let's add a method generateKeyPair () to our class Wallet and let's call it in the builder:
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"); // Inicializar el generador de claves y generar un KeyPair keyGen.initialize(ecSpec, random); //256 bytes provides an acceptable security level KeyPair keyPair = keyGen.generateKeyPair(); // Configurar las claves pública y privada desde el keyPair privateKey = keyPair.getPrivate(); publicKey = keyPair.getPublic(); }catch(Exception e) { throw new RuntimeException(e); } } }
Now that we have the outlines of our wallet class, let's take a look at the transactions.
Transactions and signatures
Each transaction will contain a certain amount of data:
- The public key (address) of the sender of the funds.
- The public key (s) of the recipient of the funds.
- The value / amount of funds to be transferred.
- Inputs, which are references to previous transactions that prove that the sender has funds to send.
- Outputs, which shows the amount of the relevant addresses received in the operation. (These outputs are referred to as inputs on new transactions)
- A cryptographic signature, which proves that the owner of the address is the one who sends the transaction and that the data has not been modified. (for example: prevent a third party from modifying the amount sent)
Let's create this new transaction class:
import java.security.*; import java.util.ArrayList; public class Transaction { public String transactionId; // este es también el hash de la transacción. public PublicKey sender; // dirección del remitente/llave pública. public PublicKey reciepient; // Dirección del destinatario/clave pública. public float value; public byte[] signature; // Esto es para evitar que nadie más gaste fondos en nuestra billetera. public ArrayList inputs = new ArrayList(); public ArrayList outputs = new ArrayList(); private static int sequence = 0; // un recuento aproximado de cuántas transacciones se han generado. // Constructor: public Transaction(PublicKey from, PublicKey to, float value, ArrayList inputs) { this.sender = from; this.reciepient = to; this.value = value; this.inputs = inputs; } // Calcula el hash de la transacción (que se utilizará como su Id) private String calulateHash() { sequence++; // aumentar la secuencia para evitar 2 transacciones idénticas con el mismo hash return StringUtil.applySha256( StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) + sequence ); } }
We must also create empty classes TransactionInput and TransactionOutput, don't worry, we can fill them in later.
Our transaction class will also contain relevant methods for generating / verifying the signature and verifying the transaction.
What is the purpose of signatures and how do they work?
Firms perform two very important tasks on our blockchain: First, they allow only the owner to spend their coins; second, they prevent others from manipulating the presented transaction before a new block is extracted (at the entry point).
The private key is used to sign the data and the public key can be used to verify its integrity.
For example: James wants to send 2 NoobCoins to Allie, so his wallet software generates this transaction and sends it to the miners to be included in the next block. A miner tries to change the recipient of the 2 coins to Juan. Fortunately, however, James had signed the transaction data with his private key, allowing anyone to verify whether the transaction data has been changed using James's public key (since no other person will be able to verify the transaction).
We can see (from the code block above) that our signature will be a bunch of bytes, so let's create a method to generate them. The first thing we'll need is a few helper functions in the class StringUtil :
//Aplica la firma ECDSA y devuelve el resultado ( en 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; } //Verifica una firma de cadena 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()); }
Now we are going to use these signature methods in our class Transaction, adding the methods generateSignature () and verifiySignature ():
//Firma todos los datos que no queremos que sean alterados. public void generateSignature(PrivateKey privateKey) { String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) ; signature = StringUtil.applyECDSASig(privateKey,data); } //Verifica que los datos que firmamos no hayan sido alterados public boolean verifiySignature() { String data = StringUtil.getStringFromKey(sender) + StringUtil.getStringFromKey(reciepient) + Float.toString(value) ; return StringUtil.verifyECDSASig(sender, data, signature); }
The signatures will be verified by the miners as a new transaction is added to a block.
Proof of wallets and signatures
Now we're almost halfway there let's try some things that are working. In the class NoobChain agreguemos algunas variables nuevas y reemplacemos el contents de nuestro método principal:
import java.security.Security; import java.util.ArrayList; import java.util.Base64; import com.google.gson.GsonBuilder; public class NoobChain { public static ArrayList blockchain = new ArrayList(); public static int difficulty = 5; public static Wallet walletA; public static Wallet walletB; public static void main(String[] args) { //Configurar el castillo de Bouncey como un proveedor de seguridad Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); //Crear las nuevas carteras walletA = new Wallet(); walletB = new Wallet(); //Probar claves públicas y privadas System.out.println("Claves privadas y públicas:"); System.out.println(StringUtil.getStringFromKey(walletA.privateKey)); System.out.println(StringUtil.getStringFromKey(walletA.publicKey)); //Crear una transacción de prueba de WalletA a WalletB Transaction transaction = new Transaction(walletA.publicKey, walletB.publicKey, 5, null); transaction.generateSignature(walletA.privateKey); //Verificar el funcionamiento de la firma y verificarla desde la clave pública System.out.println("Se verifica la firma"); System.out.println(transaction.verifiySignature()); }
Inputs & Outputs 1: Cómo se posee la divisa criptográfica….
In order for you to have one (1) bitcoin, you must receive 1 Bitcoin. The ledger does not actually add a bitcoin coin to it and minus a bitcoin coin from the sender, the sender is referencing that he / she previously received a bitcoin coin, then a transaction output was created showing that 1 Bitcoin was sent to your address. (Transaction entries are references to previous transaction exits.)
Your wallet balance is the sum of all unused transaction outputs directed at you.
From this point on we will follow the bitcoin convention and call the unused transaction outputs: UTXO's.
So let's create a transaction input class:
public class TransactionInput { public String transactionOutputId; // Referencia a TransactionOutputs -> transactionId public TransactionOutput UTXO; // Contiene la salida de la transacción no utilizada public TransactionInput(String transactionOutputId) { this.transactionOutputId = transactionOutputId; } }
And a class TransactionOutputs:
import java.security.PublicKey; public class TransactionOutput { public String id; public PublicKey reciepient; // también conocido como el nuevo dueño de estas monedas. public float value; // la cantidad de monedas que poseen public String parentTransactionId; // el identificador de la transacción en la que se creó esta salida //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); } //Comprueba si la moneda le pertenece public boolean isMine(PublicKey publicKey) { return (publicKey == reciepient); } }
The results of the transaction will show the final amount sent to each party of the transaction. These, when mentioned as entries in new transactions, act as proof that you have coins to send.
Inputs & Outputs 2: Processing the transaction….
The blocks in the chain can receive many transactions and the block chain can be very, very long, it could take eons to process a new transaction because we have to find and check its inputs. To avoid this, we will keep an extra collection of all unspent transactions that can be used as inputs. In our class NoobChain add this collection of all UTXOs:
public class NoobChain { public static ArrayList blockchain = new ArrayList(); public static HashMap<String,TransactionOutputs> UTXOs = new HashMap<String,TransactionOutputs>(); //lista de todas las transacciones no gastadas. public static int difficulty = 5; public static Wallet walletA; public static Wallet walletB; public static void main(String[] args) {
Now we are going to put everything together to process the transaction with a boolean method processTransaction in our class Transaction:
//Devuelve true si se pudiera crear una nueva transacción. public boolean processTransaction() { if(verifiySignature() == false) { System.out.println("#la Firma de la transacción no se verificó"); return false; } //Recopilar los datos de las transacciones (asegúrese de que no se hayan gastado): for(TransactionInput i : inputs) { i.UTXO = NoobChain.UTXOs.get(i.transactionOutputId); } //verificar si la transacción es válida: if(getInputsValue() < NoobChain.minimumTransaction) { System.out.println("#Insumos de transacciones a pequeños: " + getInputsValue()); return false; } //generar resultados de transacciones: float leftOver = getInputsValue() - value; //get value of inputs then the left over change: transactionId = calulateHash(); outputs.add(new TransactionOutput( this.reciepient, value,transactionId)); //enviar valor al destinatario outputs.add(new TransactionOutput( this.sender, leftOver,transactionId)); // enviar la parte de atrás 'cambiar' de nuevo al remitente //añadir salidas a la lista de productos no utilizados for(TransactionOutput o : outputs) { NoobChain.UTXOs.put(o.id , o); } //Eliminar las entradas de transacción de las listas UTXO como gastadas: 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; } //devuelve la suma de los valores de las entradas (UTXOs) public float getInputsValue() { float total = 0; for(TransactionInput i : inputs) { if(i.UTXO == null) continue; //Si no se encuentra la transacción, sáltela. 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; }
… With this method we do some checks to make sure the transaction is valid, then we collect inputs and generate outputs. (See commented lines in the code for more information).
Importantly, towards the end, we discard the inputs from our UTXO list, which means that a transaction output can only be used once as input ... Therefore, the full value of the inputs must be used, of so the sender returns the "change" to himself.
At last we are going to update our wallet to:
Gather our balance (looping through the list of UTXOs and checking if the output of a transaction is Mine ())
And generate transactions for us….
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() {... //devuelve el saldo y almacena los UTXO's propiedad de esta cartera en este.UTXOs public float getBalance() { float total = 0; for (Map.Entry<String, TransactionOutput> item: NoobChain.UTXOs.entrySet()){ TransactionOutput UTXO = item.getValue(); if(UTXO.isMine(publicKey)) { // si la salida me pertenece ( si las monedas me pertenecen) UTXOs.put(UTXO.id,UTXO); //añádalo a nuestra lista de transacciones no gastadas. total += UTXO.value ; } } return total; } //Genera y devuelve una nueva transacción desde esta cartera. public Transaction sendFunds(PublicKey _recipient,float value ) { if(getBalance() < value) { //reunir el saldo y verificar los fondos. System.out.println("#No hay fondos suficientes para enviar la transacción. Transacción descartada."); return null; } //create array list of inputs ArrayList inputs = new ArrayList(); 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; } }
Adding transactions to our blocks:
Now that we have a working transaction system, we need to implement it on our blockchain. We should replace the useless data we had in our blocks with an ArrayList of transactions. However, there can be thousands of transactions in a single block, too many to include in our hash calculation ... but don't worry, we can use the merkle root of the transactions (you can quickly read about merkle trees here * soon *).
Let's add a helper method to generate the merkleroot in StringUtils:
//Realiza una serie de transacciones y devuelve una raíz de merkle. public static String getMerkleRoot(ArrayList transactions) { int count = transactions.size(); ArrayList previousTreeLayer = new ArrayList(); for(Transaction transaction : transactions) { previousTreeLayer.add(transaction.transactionId); } ArrayList treeLayer = previousTreeLayer; while(count > 1) { treeLayer = new ArrayList(); 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; }
Now we are going to implement our changes in the block class:
import java.util.ArrayList; import java.util.Date; public class Block { public String hash; public String previousHash; public String merkleRoot; public ArrayList transactions = new ArrayList(); //our data will be a simple message. public long timeStamp; //como número de milisegundos desde 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. } //Calcular nuevo hash basado en el contenido de los bloques public String calculateHash() { String calculatedhash = StringUtil.applySha256( previousHash + Long.toString(timeStamp) + Integer.toString(nonce) + merkleRoot ); return calculatedhash; } //Aumenta el valor de nonce hasta que se alcanza el objetivo de hash. 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("Bloque minado!!! : " + hash); } //Añadir transacciones a este bloque public boolean addTransaction(Transaction transaction) { //procesar la transacción y verificar si es válida, a menos que el bloque sea un bloque de génesis y luego ignorar. 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("Transacción añadida con éxito al Bloque"); return true; } }
Note that we also update our block constructor as we don't need to pass the string data and include the root of merkle in the hash calculation method.
Our boolean method addTransaction it will add the transactions and only return true if the transaction was added successfully.
The Grand Final (In the beginning there were no coins):
We should test sending coins to and from wallets, and update our blockchain validity check. But first we need a way to introduce new coins into the mix. There are many ways to create new coins, on the bitcoin blockchain for example: miners can include a transaction for themselves as a reward for each block mined. For now, however, we will only release all the coins we wish to have, in the first block (the genesis block). Like bitcoin, we will code the genesis block.
We are going to update our NoobChain class with everything it needs:
- A Genesis block that releases 100 Noobcoins to the wallet.
- An updated string validity check that takes operations into account.
- Some test transactions to see that everything works.
public class NoobChain { public static ArrayList blockchain = new ArrayList(); 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) { //añadir nuestros bloques a la lista ArrayList de bloques: Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); //Setup Bouncey castle as a Security Provider //Crear billeteras walletA = new Wallet(); walletB = new Wallet(); Wallet coinbase = new Wallet(); //crear una transacción de génesis, que envía 100 NoobCoin a la billeteraA: genesisTransaction = new Transaction(coinbase.publicKey, walletA.publicKey, 100f, null); genesisTransaction.generateSignature(coinbase.privateKey); //firmar manualmente la transacción de génesis genesisTransaction.transactionId = "0"; //configurar manualmente el ID de la transacción genesisTransaction.outputs.add(new TransactionOutput(genesisTransaction.reciepient, genesisTransaction.value, genesisTransaction.transactionId)); //añadir manualmente la salida de transacciones UTXOs.put(genesisTransaction.outputs.get(0).id, genesisTransaction.outputs.get(0)); // es importante almacenar nuestra primera transacción en la lista de UTXOs. System.out.println("Creación y explotación del bloque Génesis.... "); Block genesis = new Block("0"); genesis.addTransaction(genesisTransaction); addBlock(genesis); //prueba Block block1 = new Block(genesis.hash); System.out.println("nEl balance de WalletA es: " + walletA.getBalance()); System.out.println("nWalletA está intentando enviar fondos (40) a WalletB..."); block1.addTransaction(walletA.sendFunds(walletB.publicKey, 40f)); addBlock(block1); System.out.println("n el balance de WalletA es: " + walletA.getBalance()); System.out.println("El balance de WalletB es: " + walletB.getBalance()); Block block2 = new Block(block1.hash); System.out.println("nWalletA Intentando enviar más fondos (1000) de los que tiene..."); block2.addTransaction(walletA.sendFunds(walletB.publicKey, 1000f)); addBlock(block2); System.out.println("nel balance de WalletA es: " + walletA.getBalance()); System.out.println("el balance de WalletB es: " + walletB.getBalance()); Block block3 = new Block(block2.hash); System.out.println("nWalletB está intentando enviar fondos (20) a WalletA..."); block3.addTransaction(walletB.sendFunds( walletA.publicKey, 20)); System.out.println("nel balance de WalletA es: " + walletA.getBalance()); System.out.println("el balance de WalletB es: " + walletB.getBalance()); isChainValid(); } public static Boolean isChainValid() { Block currentBlock; Block previousBlock; String hashTarget = new String(new char[difficulty]).replace('', '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)); //a través de la cadena de bloques para comprobar los hashes: for(int i=1; i < blockchain.size(); i++) { currentBlock = blockchain.get(i); previousBlock = blockchain.get(i-1); //comparar el hash registrado y el hash calculado: if(!currentBlock.hash.equals(currentBlock.calculateHash()) ){ System.out.println("#Hashes actuales no son iguales"); return false; } //comparar el hash anterior y el hash anterior registrado if(!previousBlock.hash.equals(currentBlock.previousHash) ) { System.out.println("#Hashes anteriores no son iguales"); return false; } //comprobar si se ha resuelto el problema del hash if(!currentBlock.hash.substring( 0, difficulty).equals(hashTarget)) { System.out.println("#Este bloque no ha sido minado"); return false; } //a través de las transacciones de bloqueo: TransactionOutput tempOutput; for(int t=0; t <currentBlock.transactions.size(); t++) { Transaction currentTransaction = currentBlock.transactions.get(t); if(!currentTransaction.verifiySignature()) { System.out.println("#La firma en la transacción (" + t + ") es inválida"); return false; } if(currentTransaction.getInputsValue() != currentTransaction.getOutputsValue()) { System.out.println("#Las entradas son iguales a las salidas en la transacción(" + t + ")"); return false; } for(TransactionInput input: currentTransaction.inputs) { tempOutput = tempUTXOs.get(input.transactionOutputId); if(tempOutput == null) { System.out.println("#Falta la entrada referenciada en Transacción(" + t + ")"); return false; } if(input.UTXO.value != tempOutput.value) { System.out.println("#El valor de la entrada referenciada Transacción(" + t + ") es inválido"); 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("#El destinatario de la salida de la transacción (" + t + ") no es quien debería ser"); return false; } if( currentTransaction.outputs.get(1).reciepient != currentTransaction.sender) { System.out.println("#Transacción(" + t + ") salida 'cambio' no es el remitente."); return false; } } } System.out.println("La Blockchain es válida"); return true; } public static void addBlock(Block newBlock) { newBlock.mineBlock(difficulty); blockchain.add(newBlock); } }
Wallets can now send funds securely on your blockchain, only if they have funds to send them. This means that you have your own local cryptocurrency.
You are done with the transactions on your blockchain!
You have successfully created your own cryptocurrency. Your Bockchain now:
- Allows users to create portfolios with `new Portfolio (); '.
- Provides public and private key wallets using elliptic curve cryptography.
- Asegura la transferencia de fondos, utilizando un algorithm de firma digital para probar la propiedad.
- And finally allow users to make transactions on their blockchain with 'Block.addTransaction (walletA.sendFunds (walletB.publicKey, 20));'; »Block.addTransaction (walletA.sendFunds (walletB.publicKey, 20)); '.
Juegos Play-to-earn
La industria de los videojuegos está pasando por una fase de crecimiento intenso, y el sistema «jugar para ganar» abre nuevas posibilidades para el juego con blockchain.
Los jugadores de este modelo de negocio crean valor para los desarrolladores del juego y otros jugadores al participar en la economía del juego. Como recompensa por su participación, reciben activos del juego. Estos activos van desde recursos del juego como herramientas de juego, armas o criptomonedas, hasta otros activos del juego que pueden ser tokenizados en la blockchain e incluso vendidos como NFT. Por ello, el modelo de negocio «play-to-earn» ha tenido éxito cuando se ha utilizado con juegos de blockchain. Los ingresos totales del juego «play-to-earn», Axie Infinity, se acercaron a los USD 120 millones en julio.