Cryptographic javascript-functions for ethereum and tutorials to use them with web3js and solidity
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
eth-crypto/tutorials/signed-data.md

7.5 KiB

Tutorial: Sign and validate data with solidity

In this tutorial we will sign data with JavaScript and later validate the signature in a solidity smart-contract.

Prerequisites

First we create two identities, creator and receiver.

const EthCrypto = require('eth-crypto');

const creatorIdentity = EthCrypto.createIdentity();
const recieverIdentity = EthCrypto.createIdentity();

Then we start a local testnet to use later. At the testnet, we give the creatorIdentity a balance of 10 ether. We also give one ether to the recieverIdentity so we have enough gas to send transactions.

const Web3 = require('web3');
const ganache = require('ganache-cli');

// create a web3-instance
const web3 = new Web3('http://'); // set 'http://' because web3 needs a provider
web3.transactionConfirmationBlocks = 1; // set confirmations-blocks to 1 for fast testing

// create a ganache-provider
const ganacheProvider = ganache.provider({
    accounts: [
        // we preset the balance of our creatorIdentity to 10 ether
        {
            secretKey: creatorIdentity.privateKey,
            balance: web3.utils.toWei('10', 'ether')
        },
        // we also give some wei to the recieverIdentity
        // so it can send transaction to the chain
        {
            secretKey: recieverIdentity.privateKey,
            balance: web3.utils.toWei('1', 'ether')
        }
    ]
});

// set ganache to web3 as provider
web3.setProvider(ganacheProvider);

Create a smart-contract that can validate signatures

Let's create an example-contract. The contract will be a donation-bag which contains some ether and has an owner. Whenever someone submits a valid donation-signature, he receives a part of the contracts value. This allows the creator of the contract to give signed data to people off-chain which they can later use to claim the value on-chain.

Write the contracts code in a file called DonationBag.sol. Check out its content here.

As you can see, the contract has some methods:

  • DonationBag(): The constructor which is called when the contract is created. Here we set the owner of the DonationBag
  • default-function: The default function is called when we send ether to the contract without doing anything. This is needed so the contract can receive value.
  • prefixedHash(): Creates a hash of the data which must be signed by the creator.
  • isSignatureValid(): Checks if a given signature is really signed by the sender and contains the correct content.
  • recieveDonation(): This is called by the receiver when the donation is claimed.

Deploy the contract

Before we can put the contract on our local blockchain. We have to compile the solidity-code to bytecode. We will do this by using the javascript-version of the solc compiler.

const path = require('path');
const SolidityCli = require('solidity-cli');
const contractPath = path.join(__dirname, '../../contracts/DonationBag.sol');
const compiled = await SolidityCli.compileFile(contractPath);
const compiledDonationBag = compiled[':DonationBag'];

console.dir(compiledDonationBag);
/* > {
    interface: [...],
    bytecode: '10390f35b34156101ef57600...'
}
 */

Now that we have the bytecode of the contract, we can submit a transaction to create a new instance of it at our local testchain.

// create contract-create-code
const createCode = EthCrypto.txDataByCompiled(
    compiledDonationBag.interface, // abi
    compiledDonationBag.bytecode, // bytecode
    [creatorIdentity.address] // constructor-arguments
);

// create a transaction the deploys the contract
const rawTx = {
    from: creatorIdentity.address,
    nonce: 0,
    gasLimit: 5000000,
    gasPrice: 5000000000,
    data: createCode
};
const serializedTx = EthCrypto.signTransaction(
    rawTx,
    creatorIdentity.privateKey
);

// submit to local chain
const receipt = await web3.eth.sendSignedTransaction(serializedTx);
const contractAddress = receipt.contractAddress;

console.log(contractAddress);
// > '0xCF3d784002721227F36575eD051Ea2171a528b7D' <- this is the address of our contract

Awesome. The contract is now on the blockchain. To check if it is deployed correctly, lets call a function on it.


// create contract instance
const contractInstance = new web3.eth.Contract(
    JSON.parse(compiledDonationBag.interface),
    contractAddress
);

// check owner
const owner = await contractInstance.methods.owner().call();
console.dir(owner); // same as creatorIdentity.address

Before we can sign donations, we have to send some value to the contract.

const rawTx2 = {
    from: creatorIdentity.address,
    to: contractAddress,
    nonce: 1, // increased by one
    value: parseInt(web3.utils.toWei('3', 'ether')),
    gasLimit: 600000,
    gasPrice: 20000000000
};
const serializedTx2 = EthCrypto.signTransaction(
    rawTx2,
    creatorIdentity.privateKey
);
await web3.eth.sendSignedTransaction(serializedTx2);

// check balance
const balance = await contractInstance.methods.getBalance().call();
console.log(balance); // > '1000000000000000000'

Sign the message

Lets sign a message with the creatorIdentity where the donator validates a donation to the recieverIdentity.

const signHash = EthCrypto.hash.keccak256([
    { // prefix
        type: 'string',
        value: 'Signed for DonationBag:'
    }, { // contractAddress
        type: 'address',
        value: contractAddress
    }, { // receiverAddress
        type: 'address',
        value: recieverIdentity.address
    }
]);

const signature = EthCrypto.sign(
    creatorIdentity.privateKey,
    signHash
);

As you can see, we did not sign the reciever-address directly but a hash that was build of some concated data:

  • Prefix: To ensure the creator cannot be tricked into accidentally singing a valid ethereum-transaction, we prefix the signed data with something unique to our system. In this case lets take the string Signed for DonationBag:.
  • contractAddress: It might be possible that the creator has more than one instance of the contract deployed to the blockchain. In this case its signatures might be replayed to other instances. As prevention of this attack, we also add the contracts address to the signed hash.
  • receiverAddress: By signing this address, the creator proves that the given address should receive the donation.

Recover the signature on the blockchain

The receiver now has a signature from the creator which he can send to the contract to claim the donation.


// we have to split the signature-hex-string into its parts
const vrs = EthCrypto.vrs.fromString(signature);
/* > {
    v: '0x1c',
    r: '0x525db3ea66...',
    s: '0x78544aebe6...'
    }
*/

// create the transaction-data for the recieveDonation()-call
const recieveCode = contractInstance
    .methods.recieveDonation(
        vrs.v,
        vrs.r,
        vrs.s
    ).encodeABI();

// create+sign the transaction
const recieveTx = {
    from: recieverIdentity.address,
    to: contractAddress,
    nonce: 0,
    gasLimit: 5000000,
    gasPrice: 5000000000,
    data: recieveCode
};
const serializedRecieveTx = EthCrypto.signTransaction(
    recieveTx,
    recieverIdentity.privateKey
);

// submit the tx
const receipt3 = await web3.eth.sendSignedTransaction(serializedRecieveTx);

If everything has gone right, the receiver should now have more ether than before. Let's check this.

const receiverBalance = await web3.eth.getBalance(recieverIdentity.address);
console.dir(receiverBalance);
// '1999802840000000000'