diff --git a/README.md b/README.md index 1943105..ad89e6a 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ import driver from 'bigchaindb-driver' ```js const driver = require('bigchaindb-driver') +const base58 = require('bs58'); +const { Ed25519Sha256 } = require('crypto-conditions'); // BigchainDB server instance (e.g. https://example.com/api/v1/) const API_PATH = 'http://localhost:9984/api/v1/' @@ -89,6 +91,21 @@ const tx = driver.Transaction.makeCreateTransaction( // Sign the transaction with private keys const txSigned = driver.Transaction.signTransaction(tx, alice.privateKey) +// Or use delegateSignTransaction to provide your own signature function +function signTransaction() { + // get privateKey from somewhere + const privateKeyBuffer = Buffer.from(base58.decode(alice.privateKey)) + return function sign(transaction, input, transactionHash) { + const ed25519Fulfillment = new Ed25519Sha256(); + ed25519Fulfillment.sign( + Buffer.from(transactionHash, 'hex'), + privateKeyBuffer + ); + return ed25519Fulfillment.serializeUri(); + }; +} +const txSigned = driver.Transaction.delegateSignTransaction(tx, signTransaction()) + // Send the transaction off to BigchainDB const conn = new driver.Connection(API_PATH) @@ -193,7 +210,7 @@ See the file named [RELEASE_PROCESS.md](RELEASE_PROCESS.md). ## Authors * inspired by [`js-bigchaindb-quickstart`](https://github.com/sohkai/js-bigchaindb-quickstart) of @sohkhai [thanks] -* BigchainDB +* BigchainDB * BigchainDB contributors ## Licenses diff --git a/docker-compose.yml b/docker-compose.yml index c9d152f..d0c9ce7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,7 +33,7 @@ services: retries: 3 command: -l DEBUG start tendermint: - image: tendermint/tendermint:0.22.8 + image: tendermint/tendermint:v0.31.5 # volumes: # - ./tmdata:/tendermint entrypoint: '' diff --git a/src/transaction.js b/src/transaction.js index f28f70a..3c6f3fb 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -255,4 +255,30 @@ export default class Transaction { signedTx.id = sha256Hash(serializedSignedTransaction) return signedTx } + + /** + * Delegate signing of the given `transaction` returning a new copy of `transaction` + * that's been signed. + * @param {Object} transaction Transaction to sign. `transaction` is not modified. + * @param {Function} signFn Function signing the transaction, expected to return the fulfillment. + * @returns {Object} The signed version of `transaction`. + */ + static delegateSignTransaction(transaction, signFn) { + const signedTx = clone(transaction) + const serializedTransaction = + Transaction.serializeTransactionIntoCanonicalString(transaction) + + signedTx.inputs.forEach((input) => { + const transactionUniqueFulfillment = input.fulfills ? serializedTransaction + .concat(input.fulfills.transaction_id) + .concat(input.fulfills.output_index) : serializedTransaction + const transactionHash = sha256Hash(transactionUniqueFulfillment) + const fulfillmentUri = signFn(input, transactionHash) + input.fulfillment = fulfillmentUri + }) + + const serializedSignedTransaction = Transaction.serializeTransactionIntoCanonicalString(signedTx) + signedTx.id = sha256Hash(serializedSignedTransaction) + return signedTx + } } diff --git a/test/constants.js b/test/constants.js index 6062fec..b1708e9 100644 --- a/test/constants.js +++ b/test/constants.js @@ -3,6 +3,8 @@ // Code is Apache-2.0 and docs are CC-BY-4.0 import test from 'ava' +import base58 from 'bs58' +import { Ed25519Sha256 } from 'crypto-conditions' import { Transaction, Ed25519Keypair } from '../src' // TODO: Find out if ava has something like conftest, if so put this there. @@ -31,6 +33,21 @@ export const bob = new Ed25519Keypair() export const bobCondition = Transaction.makeEd25519Condition(bob.publicKey) export const bobOutput = Transaction.makeOutput(bobCondition) +export function delegatedSignTransaction(...keyPairs) { + return function sign(input, transactionHash) { + const filteredKeyPairs = keyPairs.filter(({ publicKey }) => + input.owners_before.includes(publicKey)) + const ed25519Fulfillment = new Ed25519Sha256() + filteredKeyPairs.forEach(keyPair => { + const privateKey = Buffer.from(base58.decode(keyPair.privateKey)) + ed25519Fulfillment.sign( + Buffer.from(transactionHash, 'hex'), + privateKey + ) + }) + return ed25519Fulfillment.serializeUri() + } +} // TODO: https://github.com/avajs/ava/issues/1190 test('', () => 'dirty hack. TODO: Exclude this file from being run by ava') diff --git a/test/integration/test_integration.js b/test/integration/test_integration.js index b19c4a9..d132586 100644 --- a/test/integration/test_integration.js +++ b/test/integration/test_integration.js @@ -13,7 +13,8 @@ import { bob, bobOutput, asset, - metaData + metaData, + delegatedSignTransaction } from '../constants' @@ -202,6 +203,58 @@ test('Valid TRANSFER transaction with multiple Ed25519 inputs from different tra }) }) +test('Valid CREATE transaction using delegateSign with default node', t => { + const conn = new Connection() + + const tx = Transaction.makeCreateTransaction( + asset(), + metaData, + [aliceOutput], + alice.publicKey + ) + + const txSigned = Transaction.delegateSignTransaction( + tx, + delegatedSignTransaction(alice) + ) + + return conn.postTransaction(txSigned) + .then(resTx => { + t.truthy(resTx) + }) +}) + +test('Valid TRANSFER transaction with multiple Ed25519 inputs using delegateSign', t => { + const conn = new Connection(API_PATH) + const createTx = Transaction.makeCreateTransaction( + asset(), + metaData, + [aliceOutput, bobOutput], + alice.publicKey + ) + const createTxSigned = Transaction.signTransaction( + createTx, + alice.privateKey + ) + + return conn.postTransactionCommit(createTxSigned) + .then(() => { + const transferTx = Transaction.makeTransferTransaction( + [{ tx: createTxSigned, output_index: 0 }, { tx: createTxSigned, output_index: 1 }], + [Transaction.makeOutput(aliceCondition, '2')], + metaData + ) + + const transferTxSigned = Transaction.delegateSignTransaction( + transferTx, + delegatedSignTransaction(alice, bob) + ) + + return conn.postTransactionCommit(transferTxSigned) + .then(resTx => t.truthy(resTx)) + }) +}) + test('Search for spent and unspent outputs of a given public key', t => { const conn = new Connection(API_PATH) const carol = new Ed25519Keypair() diff --git a/test/transaction/test_cryptoconditions.js b/test/transaction/test_cryptoconditions.js index 80609b2..b6fe2bb 100644 --- a/test/transaction/test_cryptoconditions.js +++ b/test/transaction/test_cryptoconditions.js @@ -5,6 +5,7 @@ import test from 'ava' import cc from 'crypto-conditions' import { Ed25519Keypair, Transaction, ccJsonLoad } from '../../src' +import { delegatedSignTransaction } from '../constants' import sha256Hash from '../../src/sha256Hash' test('Ed25519 condition encoding', t => { @@ -96,6 +97,24 @@ test('Fulfillment correctly formed', t => { )) }) +test('Delegated signature is correct', t => { + const alice = new Ed25519Keypair() + + const txCreate = Transaction.makeCreateTransaction( + {}, + {}, + [Transaction.makeOutput(Transaction.makeEd25519Condition(alice.publicKey))], + alice.publicKey + ) + + const signCreateTransaction = Transaction.signTransaction(txCreate, alice.privateKey) + const delegatedSignCreateTransaction = Transaction.delegateSignTransaction( + txCreate, + delegatedSignTransaction(alice) + ) + t.deepEqual(signCreateTransaction, delegatedSignCreateTransaction) +}) + test('CryptoConditions JSON load', t => { const cond = ccJsonLoad({