From f0d3594a6ed75a322236481655f3c742417ace39 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Sat, 6 Jul 2019 19:38:14 -0700 Subject: [PATCH 1/2] CT-256 - algorand offline signing --- modules/core/src/v2/coins/algo.ts | 173 +++++++++++++++++++----- modules/core/src/v2/keychains.ts | 18 +++ modules/core/src/v2/wallet.ts | 7 +- modules/core/test/v2/fixtures/algo.ts | 4 +- modules/core/test/v2/unit/coins/algo.ts | 4 +- 5 files changed, 164 insertions(+), 42 deletions(-) diff --git a/modules/core/src/v2/coins/algo.ts b/modules/core/src/v2/coins/algo.ts index e0deac3d30..d764b000a1 100644 --- a/modules/core/src/v2/coins/algo.ts +++ b/modules/core/src/v2/coins/algo.ts @@ -3,8 +3,7 @@ */ import { BaseCoin } from '../baseCoin'; import * as _ from 'lodash'; - -const { +import { NaclWrapper, Multisig, Address, @@ -13,7 +12,69 @@ const { generateAccount, isValidAddress, isValidSeed, -} = require('algosdk'); + Encoding, +} from 'algosdk'; + +export interface TransactionExplanation { + displayOrder: string[]; + id: string; + outputs: Output[], + changeOutputs: Output[], + outputAmount: string; + changeAmount: number; + fee: TransactionFee; + memo: string; +} + +export interface SignTransactionOptions { + txPrebuild: TransactionPrebuild; + prv: string; + wallet: { + addressVersion: string; + } + keychain: KeyPair; + backupKeychain: KeyPair; + bitgoKeychain: KeyPair; +} + +export interface TransactionPrebuild { + txBase64: string; + txInfo: { + from: string; + to: string; + amount: string; + fee: number; + firstRound: number; + lastRound: number; + genesisID: string; + genesisHash: string; + note?: string; + } +} + +export interface HalfSignedTransaction { + halfSigned: { + txBase64: string, + } +} + +export interface Output { + address: string; + amount: string; +} + +export interface TransactionFee { + fee: number; + feeRate?: number; + size?: number +} + +export interface ExplainTransactionOptions { + txBase64: string; + wallet: { + addressVersion: string; + } +} interface KeyPair { pub: string; @@ -89,7 +150,7 @@ export class Algo extends BaseCoin { * @param {String} prv the prv to be checked * @returns {Boolean} is it valid? */ - isValidPrv(prv): boolean { + isValidPrv(prv: string): boolean { return isValidSeed(prv); } @@ -99,7 +160,7 @@ export class Algo extends BaseCoin { * @param {String} address the pub to be checked * @returns {Boolean} is it valid? */ - isValidAddress(address): boolean { + isValidAddress(address: string): boolean { return isValidAddress(address); } @@ -109,7 +170,7 @@ export class Algo extends BaseCoin { * @param key * @param message */ - signMessage(key, message): Buffer { + signMessage(key: KeyPair, message: string | Buffer): Buffer { // key.prv actually holds the encoded seed, but we use the prv name to avoid breaking the keypair schema. // See jsdoc comment in isValidPrv let seed = key.prv; @@ -133,30 +194,79 @@ export class Algo extends BaseCoin { } /** - * Specifies what key we will need for signing - Algorand needs the backup, bitgo pubs. + * Specifies what key we will need for signing` - Algorand needs the backup, bitgo pubs. */ - keyIdsForSigning() { + keyIdsForSigning(): number[] { return [ 0, 1, 2 ]; } + /** + * Explain/parse transaction + * @param params + * - txBase64: transaction encoded as base64 string + */ + explainTransaction(params: ExplainTransactionOptions): TransactionExplanation { + const { txBase64 } = params; + + let tx; + try { + const txToHex = Buffer.from(txBase64, 'base64'); + const decodedTx = Encoding.decode(txToHex); + + // if we are a signed msig tx, the structure actually has the { msig, txn } as the root object + // if we are not signed, the decoded tx is the txn - refer to partialSignTxn and MultiSig constructor + // in algosdk for more information + const txnForDecoding = decodedTx.txn || decodedTx; + + tx = Multisig.MultiSigTransaction.from_obj_for_encoding(txnForDecoding); + } catch (ex) { + throw new Error('txBase64 needs to be a valid tx encoded as base64 string'); + } + + const id = tx.txID(); + const fee = { fee: tx.fee}; + + const outputs = [{ + amount: tx.amount, + address: Address.encode(new Uint8Array(tx.to.publicKey)), + }]; + + const outputAmount = tx.amount; + + // TODO(CT-480): add recieving address display here + const memo = tx.note; + + return { + displayOrder: ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'memo'], + id, + outputs, + outputAmount, + changeAmount: 0, + fee, + changeOutputs: [], + memo + }; + } + /** * Assemble keychain and half-sign prebuilt transaction * * @param params * @param params.txPrebuild {Object} prebuild object returned by platform * @param params.prv {String} user prv + * @param params.wallet.addressVersion {String} this is the version of the Algorand multisig address generation format */ - signTransaction(params) { + signTransaction(params: SignTransactionOptions): HalfSignedTransaction { const prv = params.prv; - const txData = params.txPrebuild.txData; + const txBase64 = params.txPrebuild.txBase64; const addressVersion = params.wallet.addressVersion; - if (_.isUndefined(txData)) { + if (_.isUndefined(txBase64)) { throw new Error('missing txPrebuild parameter'); } - if (!_.isObject(txData)) { - throw new Error(`txPrebuild must be an object, got type ${typeof txData}`); + if (!_.isString(txBase64)) { + throw new Error(`txPrebuild must be an object, got type ${typeof txBase64}`); } if (_.isUndefined(prv)) { @@ -175,24 +285,6 @@ export class Algo extends BaseCoin { throw new Error('missing addressVersion parameter to sign transaction'); } - const refinedTxData = txData; - refinedTxData.amount = parseInt(txData.amount, 10); - - // if note is truly null, we need to pass an array - if (!_.has(txData, 'note')) { - refinedTxData.note = new Uint8Array(0); - } else { - // if note was passed as null, assume its an empty string - if (txData.note === null) { - txData.note = ''; - } - - // note has a maximum length - if (txData.note.length > MAX_ALGORAND_NOTE_LENGTH) { - throw new Error('note size exceeded specification'); - } - } - // we need to re-encode our public keys using algosdk's format const encodedPublicKeys = [ Address.decode(params.keychain.pub).publicKey, @@ -205,15 +297,28 @@ export class Algo extends BaseCoin { const pair = generateAccountFromSeed(seed); const sk = pair.sk; + // decode our tx + let transaction; + try { + const txToHex = Buffer.from(txBase64, 'base64'); + const decodedTx = Encoding.decode(txToHex); + transaction = Multisig.MultiSigTransaction.from_obj_for_encoding(decodedTx); + } catch (e) { + throw new Error('transaction needs to be a valid tx encoded as base64 string'); + } + // sign - const transaction = new Multisig.MultiSigTransaction(refinedTxData); const halfSigned = transaction.partialSignTxn( { version: addressVersion, threshold: 2, pks: encodedPublicKeys }, sk ); - const txHex = Buffer.from(halfSigned).toString('base64'); + const signedBase64 = Buffer.from(halfSigned).toString('base64'); - return { txHex }; + return { + halfSigned: { + txBase64: signedBase64, + } + }; } } diff --git a/modules/core/src/v2/keychains.ts b/modules/core/src/v2/keychains.ts index 962a92d5cc..0586f0a75d 100644 --- a/modules/core/src/v2/keychains.ts +++ b/modules/core/src/v2/keychains.ts @@ -1,6 +1,7 @@ import common = require('../common'); import * as _ from 'lodash'; import * as Promise from 'bluebird'; +const util = require('../util'); const co = Promise.coroutine; const Keychains = function(bitgo, baseCoin) { @@ -207,5 +208,22 @@ Keychains.prototype.createBackup = function(params, callback) { }).call(this).asCallback(callback); }; +/** + * Gets keys for signing from a wallet + * @param reqId + * @param callback + * @returns {Promise[]} + */ +Keychains.keysForSigning = function(params, callback) { + return co(function *() { + const reqId = params.reqId || util.createRequestId(); + const wallet = params.wallet; + const ids = wallet.baseCoin.keyIdsForSigning(); + const keychainQueriesPromises = ids.map( + keyId => wallet.baseCoin.keychains().get({ id: wallet.keys[keyId], reqId: reqId }) + ); + return yield Promise.all(keychainQueriesPromises); + }).call(this).asCallback(callback); +} module.exports = Keychains; diff --git a/modules/core/src/v2/wallet.ts b/modules/core/src/v2/wallet.ts index 790fd06d18..aa5c840ac7 100644 --- a/modules/core/src/v2/wallet.ts +++ b/modules/core/src/v2/wallet.ts @@ -14,6 +14,7 @@ import { NodeCallback } from './types'; const PendingApproval = require('./pendingApproval'); const util = require('../util'); +const Keychains = require('./keychains'); const debug = debugLib('bitgo:v2:wallet'); const co = Bluebird.coroutine; @@ -1237,11 +1238,7 @@ export class Wallet { const txPrebuildQuery = params.prebuildTx ? Promise.resolve(params.prebuildTx) : this.prebuildTransaction(params); // retrieve our keychains needed to run the prebuild - some coins use all pubs - const ids = this.baseCoin.keyIdsForSigning(); - const keychainQueriesPromises = ids.map( - keyId => this.baseCoin.keychains().get({ id: this._wallet.keys[keyId], reqId: params.reqId }) - ); - const keychains = yield Promise.all(keychainQueriesPromises); + const keychains = yield Keychains.keysForSigning({ reqId: params.reqId, wallet: this }); const txPrebuild = yield txPrebuildQuery; diff --git a/modules/core/test/v2/fixtures/algo.ts b/modules/core/test/v2/fixtures/algo.ts index 04a80f39ff..00b364e5c7 100644 --- a/modules/core/test/v2/fixtures/algo.ts +++ b/modules/core/test/v2/fixtures/algo.ts @@ -44,13 +44,15 @@ module.exports.prebuild = function() { txData: { from: 'AWSC7RL3RM72HSUW5QU4XTX3AOHY7QD3WLUZC2CAHWP6BTI5Q7IABVUXTA', to: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU', - amount: '1000', + amount: 1000, fee: 1000, firstRound: 1000, lastRound: 1999, genesisID: 'testnet-v38.0', genesisHash: '4HkOQEL2o2bVh2P1wSpky3s6cwcEg/AAd5qlery942g=', + note: new Uint8Array() }, + buildTxBase64: 'iaNhbXTNA+ijZmVlzgADsVCiZnbNA+ijZ2VurXRlc3RuZXQtdjM4LjCiZ2jEIOB5DkBC9qNm1Ydj9cEqZMt7OnMHBIPwAHeapXq8veNoomx2zQfPo3JjdsQguw62NBVKGAtqJ03XdSlcNtO6eq5rXbDMEMVGLbDzMN+jc25kxCAFpC/Fe4s/o8qW7CnLzvsDj4/Ae7LpkWhAPZ/gzR2H0KR0eXBlo3BheQ==', userKeychain: { pub: 'UMYEHZ2NNBYX43CU37LMINSHR362FT4GFVWL6V5IHPRCJVPZ46H6CBYLYE', prv: 'HEHPVOKEINTQMW4K536BZWUZWQNKBXKIAUSEWCVX3LCXDHI2LVPMV6T5L4' diff --git a/modules/core/test/v2/unit/coins/algo.ts b/modules/core/test/v2/unit/coins/algo.ts index a53c327876..a4331e1049 100644 --- a/modules/core/test/v2/unit/coins/algo.ts +++ b/modules/core/test/v2/unit/coins/algo.ts @@ -83,7 +83,7 @@ describe('ALGO:', function() { it('should sign a prebuild', co(function *() { // sign transaction halfSignedTransaction = yield wallet.signTransaction({ - txPrebuild: { txData: fixtures.txData }, + txPrebuild: { txBase64: fixtures.buildTxBase64 }, prv: fixtures.userKeychain.prv, keychain: fixtures.userKeychain, backupKeychain: fixtures.backupKeychain, @@ -91,7 +91,7 @@ describe('ALGO:', function() { wallet: { addressVersion: fixtures.walletData.coinSpecific.addressVersion } }); - halfSignedTransaction.txHex.should.equal(fixtures.signedTxBase64); + halfSignedTransaction.halfSigned.txBase64.should.equal(fixtures.signedTxBase64); })); }); }); From cfc97a64c1eb92bfefc9fea7a2063162245a5c8a Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Tue, 9 Jul 2019 15:50:58 -0700 Subject: [PATCH 2/2] CT-256 - integration test fixup --- modules/core/src/v2/keychains.ts | 11 +++++------ modules/core/src/v2/wallet.ts | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/modules/core/src/v2/keychains.ts b/modules/core/src/v2/keychains.ts index 0586f0a75d..db85cca561 100644 --- a/modules/core/src/v2/keychains.ts +++ b/modules/core/src/v2/keychains.ts @@ -214,16 +214,15 @@ Keychains.prototype.createBackup = function(params, callback) { * @param callback * @returns {Promise[]} */ -Keychains.keysForSigning = function(params, callback) { +Keychains.prototype.getKeysForSigning = function(params, callback) { return co(function *() { const reqId = params.reqId || util.createRequestId(); - const wallet = params.wallet; - const ids = wallet.baseCoin.keyIdsForSigning(); + const ids = params.wallet.baseCoin.keyIdsForSigning(); const keychainQueriesPromises = ids.map( - keyId => wallet.baseCoin.keychains().get({ id: wallet.keys[keyId], reqId: reqId }) + id => this.get({ id: params.wallet.keyIds()[id], reqId }) ); - return yield Promise.all(keychainQueriesPromises); + return Promise.all(keychainQueriesPromises); }).call(this).asCallback(callback); -} +}; module.exports = Keychains; diff --git a/modules/core/src/v2/wallet.ts b/modules/core/src/v2/wallet.ts index aa5c840ac7..a07e5440d8 100644 --- a/modules/core/src/v2/wallet.ts +++ b/modules/core/src/v2/wallet.ts @@ -1238,8 +1238,8 @@ export class Wallet { const txPrebuildQuery = params.prebuildTx ? Promise.resolve(params.prebuildTx) : this.prebuildTransaction(params); // retrieve our keychains needed to run the prebuild - some coins use all pubs - const keychains = yield Keychains.keysForSigning({ reqId: params.reqId, wallet: this }); - + const keychains = yield this.baseCoin.keychains().getKeysForSigning({ wallet: this, reqId: params.reqId }); + const txPrebuild = yield txPrebuildQuery; try {