From 077a6ffcc8010354e5818b2f42f8deccde8f64d8 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Tue, 5 Mar 2019 13:42:45 +0000 Subject: [PATCH 01/21] Improve channel rpc usage --- es/channel/handlers.js | 26 ---------------------- es/channel/index.js | 50 ++++++++---------------------------------- es/channel/internal.js | 43 +++++++++++++++++++++++++++--------- 3 files changed, 42 insertions(+), 77 deletions(-) diff --git a/es/channel/handlers.js b/es/channel/handlers.js index d8af42e741..58d18d42aa 100644 --- a/es/channel/handlers.js +++ b/es/channel/handlers.js @@ -23,7 +23,6 @@ import { send, emit } from './internal' -import * as R from 'ramda' export function awaitingConnection (channel, message, state) { if (message.method === 'channels.info') { @@ -195,31 +194,6 @@ export function awaitingUpdateConflict (channel, message, state) { } } -export function awaitingProofOfInclusion (channel, message, state) { - if (message.id === state.messageId) { - state.resolve(message.result.poi) - return { handler: channelOpen } - } - if (message.method === 'channels.error') { - state.reject(new Error(message.data.message)) - return { handler: channelOpen } - } -} - -export function awaitingBalances (channel, message, state) { - if (message.id === state.messageId) { - state.resolve(R.reduce((acc, item) => ({ - ...acc, - [item.account]: item.balance - }), {}, message.result)) - return { handler: channelOpen } - } - if (message.method === 'channels.error') { - state.reject(new Error(message.data.message)) - return { handler: channelOpen } - } -} - export async function awaitingShutdownTx (channel, message, state) { if (message.method === 'channels.sign.shutdown_sign') { const signedTx = await Promise.resolve(state.sign(message.params.data.tx)) diff --git a/es/channel/index.js b/es/channel/index.js index 80fdf79340..8ede21cec3 100644 --- a/es/channel/index.js +++ b/es/channel/index.js @@ -31,8 +31,9 @@ import { initialize, enqueueAction, send, - messageId + call } from './internal' +import * as R from 'ramda' /** * Register event listener function @@ -120,26 +121,8 @@ function update (from, to, amount, sign) { * contracts: ['ct_2dCUAWYZdrWfACz3a2faJeKVTVrfDYxCQHCqAt5zM15f3u2UfA'] * }).then(poi => console.log(poi)) */ -function poi ({ accounts, contracts }) { - return new Promise((resolve, reject) => { - enqueueAction( - this, - (channel, state) => state.handler === handlers.channelOpen, - (channel, state) => { - const id = messageId(channel) - send(channel, { - jsonrpc: '2.0', - id, - method: 'channels.get.poi', - params: { accounts, contracts } - }) - return { - handler: handlers.awaitingProofOfInclusion, - state: { resolve, reject, messageId: id } - } - } - ) - }) +async function poi ({ accounts, contracts }) { + return (await call(this, 'channels.get.poi', { accounts, contracts })).poi } /** @@ -155,26 +138,11 @@ function poi ({ accounts, contracts }) { * console.log(balances['ak_Y1NRjHuoc3CGMYMvCmdHSBpJsMDR6Ra2t5zjhRcbtMeXXLpLH']) * ) */ -function balances (accounts) { - return new Promise((resolve, reject) => { - enqueueAction( - this, - (channel, state) => state.handler === handlers.channelOpen, - (channel, state) => { - const id = messageId(channel) - send(channel, { - jsonrpc: '2.0', - id, - method: 'channels.get.balances', - params: { accounts } - }) - return { - handler: handlers.awaitingBalances, - state: { resolve, reject, messageId: id } - } - } - ) - }) +async function balances (accounts) { + return R.reduce((acc, item) => ({ + ...acc, + [item.account]: item.balance + }), {}, await call(this, 'channels.get.balances', { accounts })) } /** diff --git a/es/channel/internal.js b/es/channel/internal.js index c5dcc9a557..8dc0b38d44 100644 --- a/es/channel/internal.js +++ b/es/channel/internal.js @@ -32,6 +32,7 @@ const messageQueueLocked = new WeakMap() const actionQueue = new WeakMap() const actionQueueLocked = new WeakMap() const sequence = new WeakMap() +const rpcCallbacks = new WeakMap() function channelURL (url, { endpoint = 'channel', ...params }) { const paramString = R.join('&', R.values(R.mapObjIndexed((value, key) => @@ -103,12 +104,6 @@ async function handleMessage (channel, message) { enterState(channel, await Promise.resolve(handler(channel, message, state))) } -async function enqueueMessage (channel, message) { - const queue = messageQueue.get(channel) || [] - messageQueue.set(channel, [...queue, JSON.parse(message)]) - dequeueMessage(channel) -} - async function dequeueMessage (channel) { const queue = messageQueue.get(channel) if (messageQueueLocked.get(channel) || !queue.length) { @@ -122,8 +117,35 @@ async function dequeueMessage (channel) { dequeueMessage(channel) } -function messageId (channel) { - return sequence.set(channel, sequence.get(channel) + 1).get(channel) + +function onMessage (channel, data) { + const message = JSON.parse(data) + if (message.id) { + const callback = rpcCallbacks.get(channel).get(message.id) + try { + callback(message) + } catch (error) { + emit(channel, 'error', error) + } finally { + rpcCallbacks.get(channel).delete(message.id) + } + } else if (message.method === 'channels.message') { + emit(channel, 'message', message.params.data.message) + } else { + messageQueue.set(channel, [...(messageQueue.get(channel) || []), message]) + dequeueMessage(channel) + } +} + +function call (channel, method, params) { + return new Promise((resolve, reject) => { + const id = sequence.set(channel, sequence.get(channel) + 1).get(channel) + rpcCallbacks.get(channel).set(id, (message) => { + if (message.result) return resolve(message.result) + if (message.error) return reject(new Error(message.error.message)) + }) + send(channel, { jsonrpc: '2.0', method, id, params }) + }) } function WebSocket (url, callbacks) { @@ -167,10 +189,11 @@ async function initialize (channel, channelOptions) { fsm.set(channel, { handler: awaitingConnection }) eventEmitters.set(channel, new EventEmitter()) sequence.set(channel, 0) + rpcCallbacks.set(channel, new Map()) websockets.set(channel, await WebSocket(channelURL(channelOptions.url, { ...params, protocol: 'json-rpc' }), { onopen: () => changeStatus(channel, 'connected'), onclose: () => changeStatus(channel, 'disconnected'), - onmessage: ({ data }) => enqueueMessage(channel, data) + onmessage: ({ data }) => onMessage(channel, data) })) } @@ -185,5 +208,5 @@ export { changeState, send, enqueueAction, - messageId + call } From 6e4b3b4460b2fb32378e795fff5a27b70101278f Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Tue, 5 Mar 2019 13:47:19 +0000 Subject: [PATCH 02/21] Fix lint error --- es/channel/internal.js | 1 - 1 file changed, 1 deletion(-) diff --git a/es/channel/internal.js b/es/channel/internal.js index 8dc0b38d44..e2518881be 100644 --- a/es/channel/internal.js +++ b/es/channel/internal.js @@ -117,7 +117,6 @@ async function dequeueMessage (channel) { dequeueMessage(channel) } - function onMessage (channel, data) { const message = JSON.parse(data) if (message.id) { From 528b6c36dc0ff459a58238dac377a58f34a902f1 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Tue, 5 Mar 2019 13:58:51 +0000 Subject: [PATCH 03/21] Remove unreachable code --- es/channel/handlers.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/es/channel/handlers.js b/es/channel/handlers.js index 58d18d42aa..c94f06cf57 100644 --- a/es/channel/handlers.js +++ b/es/channel/handlers.js @@ -123,9 +123,6 @@ export async function channelOpen (channel, message, state) { case 'channels.leave': // TODO: emit event return { handler: channelOpen } - case 'channels.message': - emit(channel, 'message', message.params.data.message) - return { handler: channelOpen } case 'channels.update': changeState(channel, message.params.data.state) return { handler: channelOpen } From cb53c32854f4bd14f59d6b8d7f4f4d5661abba06 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Tue, 5 Mar 2019 21:14:06 +0000 Subject: [PATCH 04/21] Make sure that sign function is correctly called --- es/channel/handlers.js | 2 +- test/integration/channel.js | 107 +++++++++++++++++++++++------------- 2 files changed, 71 insertions(+), 38 deletions(-) diff --git a/es/channel/handlers.js b/es/channel/handlers.js index d8af42e741..38cf3bb1d5 100644 --- a/es/channel/handlers.js +++ b/es/channel/handlers.js @@ -51,7 +51,7 @@ export async function awaitingChannelCreateTx (channel, message, state) { responder: 'responder_sign' }[options.get(channel).role] if (message.method === `channels.sign.${tag}`) { - const signedTx = await options.get(channel).sign(message.tag, message.params.data.tx) + const signedTx = await options.get(channel).sign(tag, message.params.data.tx) send(channel, { jsonrpc: '2.0', method: `channels.${tag}`, params: { tx: signedTx } }) return { handler: awaitingOnChainTx } } diff --git a/test/integration/channel.js b/test/integration/channel.js index 2ecdda65bd..1b68215391 100644 --- a/test/integration/channel.js +++ b/test/integration/channel.js @@ -16,7 +16,7 @@ */ import { describe, it, before } from 'mocha' -import { spy } from 'sinon' +import * as sinon from 'sinon' import { configure, ready, plan, BaseAe, networkId } from './' import { generateKeyPair } from '../../es/utils/crypto' import Channel from '../../es/channel' @@ -46,12 +46,13 @@ describe('Channel', function () { let responderShouldRejectUpdate let existingChannelId let offchainTx - const responderSign = async (tag, tx) => { - if (!responderShouldRejectUpdate) { - return await responder.signTransaction(tx) + const initiatorSign = sinon.spy((tag, tx) => initiator.signTransaction(tx)) + const responderSign = sinon.spy((tag, tx) => { + if (responderShouldRejectUpdate) { + return null } - return null - } + return responder.signTransaction(tx) + }) const sharedParams = { url: wsUrl, pushAmount: 3, @@ -77,18 +78,27 @@ describe('Channel', function () { responderShouldRejectUpdate = false }) + afterEach(() => { + initiatorSign.resetHistory() + responderSign.resetHistory() + }) + it('can open a channel', async () => { initiatorCh = await Channel({ ...sharedParams, role: 'initiator', - sign: async (tag, tx) => await initiator.signTransaction(tx) + sign: initiatorSign }) responderCh = await Channel({ ...sharedParams, role: 'responder', sign: responderSign }) - return Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) + await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) + sinon.assert.calledOnce(initiatorSign) + sinon.assert.calledWithExactly(initiatorSign, sinon.match('initiator_sign'), sinon.match.string) + sinon.assert.calledOnce(responderSign) + sinon.assert.calledWithExactly(responderSign, sinon.match('responder_sign'), sinon.match.string) }) it('can post update and accept', async () => { @@ -101,6 +111,9 @@ describe('Channel', function () { ) result.accepted.should.equal(true) result.state.should.be.a('string') + sinon.assert.notCalled(initiatorSign) + sinon.assert.calledOnce(responderSign) + sinon.assert.calledWithExactly(responderSign, sinon.match('update_ack'), sinon.match.string) }) it('can post update and reject', async () => { @@ -112,6 +125,9 @@ describe('Channel', function () { async (tx) => await initiator.signTransaction(tx) ) result.accepted.should.equal(false) + sinon.assert.notCalled(initiatorSign) + sinon.assert.calledOnce(responderSign) + sinon.assert.calledWithExactly(responderSign, sinon.match('update_ack'), sinon.match.string) }) it('can get proof of inclusion', async () => { @@ -154,9 +170,9 @@ describe('Channel', function () { }) it('can request a withdraw and accept', async () => { - const onOnChainTx = spy() - const onOwnWithdrawLocked = spy() - const onWithdrawLocked = spy() + const onOnChainTx = sinon.spy() + const onOwnWithdrawLocked = sinon.spy() + const onWithdrawLocked = sinon.spy() responderShouldRejectUpdate = false const result = await initiatorCh.withdraw( 2, @@ -164,16 +180,19 @@ describe('Channel', function () { { onOnChainTx, onOwnWithdrawLocked, onWithdrawLocked } ) result.should.eql({ accepted: true, state: initiatorCh.state() }) - onOnChainTx.callCount.should.equal(1) - onOnChainTx.getCall(0).args[0].should.be.a('string') - onOwnWithdrawLocked.callCount.should.equal(1) - onWithdrawLocked.callCount.should.equal(1) + sinon.assert.calledOnce(onOnChainTx) + sinon.assert.calledWithExactly(onOnChainTx, sinon.match.string) + sinon.assert.calledOnce(onOwnWithdrawLocked) + sinon.assert.calledOnce(onWithdrawLocked) + sinon.assert.notCalled(initiatorSign) + sinon.assert.calledOnce(responderSign) + sinon.assert.calledWithExactly(responderSign, sinon.match('withdraw_ack'), sinon.match.string) }) it('can request a withdraw and reject', async () => { - const onOnChainTx = spy() - const onOwnWithdrawLocked = spy() - const onWithdrawLocked = spy() + const onOnChainTx = sinon.spy() + const onOwnWithdrawLocked = sinon.spy() + const onWithdrawLocked = sinon.spy() responderShouldRejectUpdate = true const result = await initiatorCh.withdraw( 2, @@ -181,15 +200,18 @@ describe('Channel', function () { { onOnChainTx, onOwnWithdrawLocked, onWithdrawLocked } ) result.should.eql({ accepted: false }) - onOnChainTx.callCount.should.equal(0) - onOwnWithdrawLocked.callCount.should.equal(0) - onWithdrawLocked.callCount.should.equal(0) + sinon.assert.notCalled(onOnChainTx) + sinon.assert.notCalled(onOwnWithdrawLocked) + sinon.assert.notCalled(onWithdrawLocked) + sinon.assert.notCalled(initiatorSign) + sinon.assert.calledOnce(responderSign) + sinon.assert.calledWithExactly(responderSign, sinon.match('withdraw_ack'), sinon.match.string) }) it('can request a deposit and accept', async () => { - const onOnChainTx = spy() - const onOwnDepositLocked = spy() - const onDepositLocked = spy() + const onOnChainTx = sinon.spy() + const onOwnDepositLocked = sinon.spy() + const onDepositLocked = sinon.spy() responderShouldRejectUpdate = false const result = await initiatorCh.deposit( 2, @@ -197,16 +219,19 @@ describe('Channel', function () { { onOnChainTx, onOwnDepositLocked, onDepositLocked } ) result.should.eql({ accepted: true, state: initiatorCh.state() }) - onOnChainTx.callCount.should.equal(1) - onOnChainTx.getCall(0).args[0].should.be.a('string') - onOwnDepositLocked.callCount.should.equal(1) - onDepositLocked.callCount.should.equal(1) + sinon.assert.calledOnce(onOnChainTx) + sinon.assert.calledWithExactly(onOnChainTx, sinon.match.string) + sinon.assert.calledOnce(onOwnDepositLocked) + sinon.assert.calledOnce(onDepositLocked) + sinon.assert.notCalled(initiatorSign) + sinon.assert.calledOnce(responderSign) + sinon.assert.calledWithExactly(responderSign, sinon.match('deposit_ack'), sinon.match.string) }) it('can request a deposit and reject', async () => { - const onOnChainTx = spy() - const onOwnDepositLocked = spy() - const onDepositLocked = spy() + const onOnChainTx = sinon.spy() + const onOwnDepositLocked = sinon.spy() + const onDepositLocked = sinon.spy() responderShouldRejectUpdate = true const result = await initiatorCh.deposit( 2, @@ -214,21 +239,27 @@ describe('Channel', function () { { onOnChainTx, onOwnDepositLocked, onDepositLocked } ) result.should.eql({ accepted: false }) - onOnChainTx.callCount.should.equal(0) - onOwnDepositLocked.callCount.should.equal(0) - onDepositLocked.callCount.should.equal(0) + sinon.assert.notCalled(onOnChainTx) + sinon.assert.notCalled(onOwnDepositLocked) + sinon.assert.notCalled(onDepositLocked) + sinon.assert.notCalled(initiatorSign) + sinon.assert.calledOnce(responderSign) + sinon.assert.calledWithExactly(responderSign, sinon.match('deposit_ack'), sinon.match.string) }) it('can close a channel', async () => { const tx = await initiatorCh.shutdown(async (tx) => await initiator.signTransaction(tx)) tx.should.be.a('string') + sinon.assert.notCalled(initiatorSign) + sinon.assert.calledOnce(responderSign) + sinon.assert.calledWithExactly(responderSign, sinon.match('shutdown_sign_ack'), sinon.match.string) }) it('can leave a channel', async () => { initiatorCh = await Channel({ ...sharedParams, role: 'initiator', - sign: async (tag, tx) => await initiator.signTransaction(tx) + sign: initiatorSign }) responderCh = await Channel({ ...sharedParams, @@ -249,15 +280,17 @@ describe('Channel', function () { role: 'initiator', existingChannelId, offchainTx, - sign: async (tag, tx) => await initiator.signTransaction(tx) + sign: initiatorSign }) responderCh = await Channel({ ...sharedParams, role: 'responder', existingChannelId, offchainTx, - sign: async (tag, tx) => await responder.signTransaction(tx) + sign: responderSign }) await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) + sinon.assert.notCalled(initiatorSign) + sinon.assert.notCalled(responderSign) }) }) From 81843608893d96dc9b52f57009085ecc69efe4f3 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Wed, 6 Mar 2019 18:37:04 +0000 Subject: [PATCH 05/21] Improve error handling for update method --- es/channel/handlers.js | 15 +++++++++++++++ test/integration/channel.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/es/channel/handlers.js b/es/channel/handlers.js index 38cf3bb1d5..629a47dd80 100644 --- a/es/channel/handlers.js +++ b/es/channel/handlers.js @@ -149,6 +149,17 @@ export async function awaitingOffChainTx (channel, message, state) { state.reject(new Error(message.data.message)) return { handler: channelOpen } } + if (message.error) { + const { data = [] } = message.error + if (data.find(i => i.code === 1001)) { + state.reject(new Error('Insufficient balance')) + } else if (data.find(i => i.code === 1002)) { + state.reject(new Error('Amount cannot be negative')) + } else { + state.reject(new Error(message.error.message)) + } + return { handler: channelOpen } + } } export function awaitingOffChainUpdate (channel, message, state) { @@ -161,6 +172,10 @@ export function awaitingOffChainUpdate (channel, message, state) { state.resolve({ accepted: false }) return { handler: channelOpen } } + if (message.error) { + state.reject(new Error(message.error.message)) + return { handler: channelOpen } + } } export async function awaitingTxSignRequest (channel, message, state) { diff --git a/test/integration/channel.js b/test/integration/channel.js index 1b68215391..931e6e5a96 100644 --- a/test/integration/channel.js +++ b/test/integration/channel.js @@ -293,4 +293,35 @@ describe('Channel', function () { sinon.assert.notCalled(initiatorSign) sinon.assert.notCalled(responderSign) }) + + describe('throws errors', function () { + async function update ({ from, to, amount, sign }) { + return initiatorCh.update( + from || await initiator.address(), + to || await responder.address(), + amount || 1, + sign || initiator.signTransaction + ) + } + + it('when posting an update with negative amount', async () => { + return update({ amount: -10 }).should.eventually.be.rejectedWith('Amount cannot be negative') + }) + + it('when posting an update with insufficient balance', async () => { + return update({ amount: 2000000000000000 }).should.eventually.be.rejectedWith('Insufficient balance') + }) + + it('when posting an update with incorrect address', async () => { + return update({ from: 'ak_123' }).should.eventually.be.rejectedWith('Rejected') + }) + + it('when posting an update with incorrect amount', async () => { + return update({ amount: '1' }).should.eventually.be.rejectedWith('Internal error') + }) + + it('when posting incorrect update tx', async () => { + return update({ sign: () => 'abcdefg' }).should.eventually.be.rejectedWith('Internal error') + }) + }) }) From 81d4f43b552ebe3a30a3cf434b8ba6ec9abb6d68 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Thu, 7 Mar 2019 15:22:36 +0000 Subject: [PATCH 06/21] Add missing channel tx serializations --- es/tx/builder/index.js | 3 + es/tx/builder/schema.js | 88 +++++++++++++++++++++++-- test/integration/channel.js | 124 ++++++++++++++++++++++++++++++++---- 3 files changed, 196 insertions(+), 19 deletions(-) diff --git a/es/tx/builder/index.js b/es/tx/builder/index.js index e06511683d..177c47f974 100644 --- a/es/tx/builder/index.js +++ b/es/tx/builder/index.js @@ -47,6 +47,9 @@ function deserializeField (value, type, prefix) { return unpackTx(value, true) case FIELD_TYPES.offChainUpdates: return value.map(v => unpackTx(v, true)) + case FIELD_TYPES.callStack: + // TODO: fix this + return [readInt(value)] default: return value } diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index 1d295a931b..ed992d646a 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -44,6 +44,11 @@ const OBJECT_TAG_CHANNEL_WITHRAW_TX = 52 const OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX = 53 const OBJECT_TAG_CHANNEL_SETTLE_TX = 56 const OBJECT_TAG_CHANNEL_OFFCHAIN_TX = 57 +const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX = 570 +const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX = 571 +const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX = 572 +const OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX = 573 +const OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX = 574 const TX_FIELD = (name, type, prefix) => [name, type, prefix] const TX_SCHEMA_FIELD = (schema, objectId) => [schema, objectId] @@ -103,7 +108,13 @@ export const TX_TYPE = { channelCloseMutual: 'channelCloseMutual', channelDeposit: 'channelDeposit', channelWithdraw: 'channelWithdraw', - channelSettle: 'channelSettle' + channelSettle: 'channelSettle', + channelOffChain: 'channelOffChain', + channelOffChainUpdateTransfer: 'channelOffChainUpdateTransfer', + channelOffChainUpdateDeposit: 'channelOffChainUpdateDeposit', + channelOffChainUpdateWithdrawal: 'channelOffChainUpdateWithdrawal', + channelOffChainCreateContract: 'channelOffChainCreateContract', + channelOffChainCallContract: 'channelOffChainCallContract' } export const OBJECT_ID_TX_TYPE = { @@ -127,7 +138,13 @@ export const OBJECT_ID_TX_TYPE = { [OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX]: TX_TYPE.channelCloseMutual, [OBJECT_TAG_CHANNEL_DEPOSIT_TX]: TX_TYPE.channelDeposit, [OBJECT_TAG_CHANNEL_WITHRAW_TX]: TX_TYPE.channelWithdraw, - [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_TYPE.channelSettle + [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_TYPE.channelSettle, + [OBJECT_TAG_CHANNEL_OFFCHAIN_TX]: TX_TYPE.channelOffChain, + [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX]: TX_TYPE.channelOffChainUpdateTransfer, + [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX]: TX_TYPE.channelOffChainUpdateDeposit, + [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX]: TX_TYPE.channelOffChainUpdateWithdrawal, + [OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX]: TX_TYPE.channelOffChainCreateContract, + [OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX]: TX_TYPE.channelOffChainCallContract } export const FIELD_TYPES = { @@ -138,7 +155,8 @@ export const FIELD_TYPES = { rlpBinary: 'rlpBinary', signatures: 'signatures', pointers: 'pointers', - offChainUpdates: 'offChainUpdates' + offChainUpdates: 'offChainUpdates', + callStack: 'callStack' } // FEE CALCULATION @@ -422,6 +440,54 @@ const CHANNEL_SETTLE_TX = [ TX_FIELD('nonce', FIELD_TYPES.int) ] +const CHANNEL_OFFCHAIN_TX = [ + ...BASE_TX, + TX_FIELD('channelId', FIELD_TYPES.id, 'ch'), + TX_FIELD('round', FIELD_TYPES.int), + TX_FIELD('updates', FIELD_TYPES.offChainUpdates), + TX_FIELD('stateHash', FIELD_TYPES.binary, 'st') +] + +const CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX = [ + ...BASE_TX, + TX_FIELD('owner', FIELD_TYPES.id, 'ak'), + TX_FIELD('ctVersion', FIELD_TYPES.int), + TX_FIELD('code', FIELD_TYPES.binary, 'cb'), + TX_FIELD('deposit', FIELD_TYPES.int), + TX_FIELD('callData', FIELD_TYPES.binary, 'cb') +] + +const CHANNEL_OFFCHAIN_CALL_CONTRACT_TX = [ + ...BASE_TX, + TX_FIELD('caller', FIELD_TYPES.id, 'ak'), + TX_FIELD('contract', FIELD_TYPES.id, 'ct'), + TX_FIELD('abiVersion', FIELD_TYPES.int), + TX_FIELD('amount', FIELD_TYPES.int), + TX_FIELD('callData', FIELD_TYPES.binary, 'cb'), + TX_FIELD('callStack', FIELD_TYPES.callStack), + TX_FIELD('gasPrice', FIELD_TYPES.int), + TX_FIELD('gasLimit', FIELD_TYPES.int) +] + +const CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX = [ + ...BASE_TX, + TX_FIELD('from', FIELD_TYPES.id, 'ak'), + TX_FIELD('to', FIELD_TYPES.id, 'ak'), + TX_FIELD('amount', FIELD_TYPES.int) +] + +const CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX = [ + ...BASE_TX, + TX_FIELD('from', FIELD_TYPES.id, 'ak'), + TX_FIELD('amount', FIELD_TYPES.int) +] + +const CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX = [ + ...BASE_TX, + TX_FIELD('from', FIELD_TYPES.id, 'ak'), + TX_FIELD('amount', FIELD_TYPES.int) +] + export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.signed]: TX_SCHEMA_FIELD(SIGNED_TX, OBJECT_TAG_SIGNED_TRANSACTION), [TX_TYPE.spend]: TX_SCHEMA_FIELD(SPEND_TX, OBJECT_TAG_SPEND_TRANSACTION), @@ -440,7 +506,13 @@ export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.channelCloseMutual]: TX_SCHEMA_FIELD(CHANNEL_CLOSE_MUTUAL_TX, OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX), [TX_TYPE.channelDeposit]: TX_SCHEMA_FIELD(CHANNEL_DEPOSIT_TX, OBJECT_TAG_CHANNEL_DEPOSIT_TX), [TX_TYPE.channelWithdraw]: TX_SCHEMA_FIELD(CHANNEL_WITHDRAW_TX, OBJECT_TAG_CHANNEL_WITHRAW_TX), - [TX_TYPE.channelSettle]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX) + [TX_TYPE.channelSettle]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX), + [TX_TYPE.channelOffChain]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_TX), + [TX_TYPE.channelOffChainUpdateTransfer]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX), + [TX_TYPE.channelOffChainUpdateDeposit]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX), + [TX_TYPE.channelOffChainUpdateWithdrawal]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX), + [TX_TYPE.channelOffChainCreateContract]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX), + [TX_TYPE.channelOffChainCallContract]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CALL_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX) } export const TX_DESERIALIZATION_SCHEMA = { @@ -461,7 +533,13 @@ export const TX_DESERIALIZATION_SCHEMA = { [OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX]: TX_SCHEMA_FIELD(CHANNEL_CLOSE_MUTUAL_TX, OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX), [OBJECT_TAG_CHANNEL_DEPOSIT_TX]: TX_SCHEMA_FIELD(CHANNEL_DEPOSIT_TX, OBJECT_TAG_CHANNEL_DEPOSIT_TX), [OBJECT_TAG_CHANNEL_WITHRAW_TX]: TX_SCHEMA_FIELD(CHANNEL_WITHDRAW_TX, OBJECT_TAG_CHANNEL_WITHRAW_TX), - [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX) + [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX), + [OBJECT_TAG_CHANNEL_OFFCHAIN_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_TX), + [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX), + [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX), + [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX), + [OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX), + [OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CALL_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX) } // VERIFICATION SCHEMA diff --git a/test/integration/channel.js b/test/integration/channel.js index 931e6e5a96..6411d55837 100644 --- a/test/integration/channel.js +++ b/test/integration/channel.js @@ -19,6 +19,7 @@ import { describe, it, before } from 'mocha' import * as sinon from 'sinon' import { configure, ready, plan, BaseAe, networkId } from './' import { generateKeyPair } from '../../es/utils/crypto' +import { unpackTx } from '../../es/tx/builder' import Channel from '../../es/channel' const wsUrl = process.env.WS_URL || 'ws://node:3014' @@ -99,35 +100,77 @@ describe('Channel', function () { sinon.assert.calledWithExactly(initiatorSign, sinon.match('initiator_sign'), sinon.match.string) sinon.assert.calledOnce(responderSign) sinon.assert.calledWithExactly(responderSign, sinon.match('responder_sign'), sinon.match.string) + const expectedTxParams = { + initiator: await initiator.address(), + responder: await responder.address(), + initiatorAmount: sharedParams.initiatorAmount.toString(), + responderAmount: sharedParams.responderAmount.toString(), + channelReserve: sharedParams.channelReserve.toString(), + // TODO: investigate why ttl is "0" + // ttl: sharedParams.ttl.toString(), + lockPeriod: sharedParams.lockPeriod.toString() + } + const { txType: initiatorTxType, tx: initiatorTx } = unpackTx(initiatorSign.firstCall.args[1]) + const { txType: responderTxType, tx: responderTx } = unpackTx(responderSign.firstCall.args[1]) + initiatorTxType.should.equal('channelCreate') + initiatorTx.should.eql({ ...initiatorTx, ...expectedTxParams }) + responderTxType.should.equal('channelCreate') + responderTx.should.eql({ ...responderTx, ...expectedTxParams }) }) it('can post update and accept', async () => { responderShouldRejectUpdate = false + const sign = sinon.spy(initiator.signTransaction.bind(initiator)) + const amount = 1 const result = await initiatorCh.update( await initiator.address(), await responder.address(), - 1, - async (tx) => await initiator.signTransaction(tx) + amount, + sign ) result.accepted.should.equal(true) result.state.should.be.a('string') sinon.assert.notCalled(initiatorSign) sinon.assert.calledOnce(responderSign) sinon.assert.calledWithExactly(responderSign, sinon.match('update_ack'), sinon.match.string) + sinon.assert.calledOnce(sign) + sinon.assert.calledWithExactly(sign, sinon.match.string) + const { txType, tx: { updates } } = unpackTx(sign.firstCall.args[0]) + txType.should.equal('channelOffChain') + updates[0].txType.should.equal('channelOffChainUpdateTransfer') + updates[0].tx.should.eql({ + ...updates[0].tx, + from: await initiator.address(), + to: await responder.address(), + amount: amount.toString() + }) }) it('can post update and reject', async () => { responderShouldRejectUpdate = true + const sign = sinon.spy(initiator.signTransaction.bind(initiator)) + const amount = 1 const result = await initiatorCh.update( await responder.address(), await initiator.address(), - 1, - async (tx) => await initiator.signTransaction(tx) + amount, + sign ) result.accepted.should.equal(false) sinon.assert.notCalled(initiatorSign) sinon.assert.calledOnce(responderSign) sinon.assert.calledWithExactly(responderSign, sinon.match('update_ack'), sinon.match.string) + sinon.assert.calledOnce(sign) + sinon.assert.calledWithExactly(sign, sinon.match.string) + const { txType, tx: { updates } } = unpackTx(sign.firstCall.args[0]) + txType.should.equal('channelOffChain') + updates[0].txType.should.equal('channelOffChainUpdateTransfer') + updates[0].tx.should.eql({ + ...updates[0].tx, + from: await responder.address(), + to: await initiator.address(), + amount: amount.toString() + }) }) it('can get proof of inclusion', async () => { @@ -138,6 +181,7 @@ describe('Channel', function () { const responderPoi = await responderCh.poi(params) initiatorPoi.should.be.a('string') responderPoi.should.be.a('string') + // TODO: proof of inclusion deserialization }) it('can get balances', async () => { @@ -170,13 +214,15 @@ describe('Channel', function () { }) it('can request a withdraw and accept', async () => { + const sign = sinon.spy(initiator.signTransaction.bind(initiator)) + const amount = 2 const onOnChainTx = sinon.spy() const onOwnWithdrawLocked = sinon.spy() const onWithdrawLocked = sinon.spy() responderShouldRejectUpdate = false const result = await initiatorCh.withdraw( - 2, - async (tx) => initiator.signTransaction(tx), + amount, + sign, { onOnChainTx, onOwnWithdrawLocked, onWithdrawLocked } ) result.should.eql({ accepted: true, state: initiatorCh.state() }) @@ -187,16 +233,27 @@ describe('Channel', function () { sinon.assert.notCalled(initiatorSign) sinon.assert.calledOnce(responderSign) sinon.assert.calledWithExactly(responderSign, sinon.match('withdraw_ack'), sinon.match.string) + sinon.assert.calledOnce(sign) + sinon.assert.calledWithExactly(sign, sinon.match.string) + const { txType, tx } = unpackTx(sign.firstCall.args[0]) + txType.should.equal('channelWithdraw') + tx.should.eql({ + ...tx, + toId: await initiator.address(), + amount: amount.toString() + }) }) it('can request a withdraw and reject', async () => { + const sign = sinon.spy(initiator.signTransaction.bind(initiator)) + const amount = 2 const onOnChainTx = sinon.spy() const onOwnWithdrawLocked = sinon.spy() const onWithdrawLocked = sinon.spy() responderShouldRejectUpdate = true const result = await initiatorCh.withdraw( - 2, - async (tx) => initiator.signTransaction(tx), + amount, + sign, { onOnChainTx, onOwnWithdrawLocked, onWithdrawLocked } ) result.should.eql({ accepted: false }) @@ -206,16 +263,27 @@ describe('Channel', function () { sinon.assert.notCalled(initiatorSign) sinon.assert.calledOnce(responderSign) sinon.assert.calledWithExactly(responderSign, sinon.match('withdraw_ack'), sinon.match.string) + sinon.assert.calledOnce(sign) + sinon.assert.calledWithExactly(sign, sinon.match.string) + const { txType, tx } = unpackTx(sign.firstCall.args[0]) + txType.should.equal('channelWithdraw') + tx.should.eql({ + ...tx, + toId: await initiator.address(), + amount: amount.toString() + }) }) it('can request a deposit and accept', async () => { + const sign = sinon.spy(initiator.signTransaction.bind(initiator)) + const amount = 2 const onOnChainTx = sinon.spy() const onOwnDepositLocked = sinon.spy() const onDepositLocked = sinon.spy() responderShouldRejectUpdate = false const result = await initiatorCh.deposit( - 2, - async (tx) => initiator.signTransaction(tx), + amount, + sign, { onOnChainTx, onOwnDepositLocked, onDepositLocked } ) result.should.eql({ accepted: true, state: initiatorCh.state() }) @@ -226,16 +294,27 @@ describe('Channel', function () { sinon.assert.notCalled(initiatorSign) sinon.assert.calledOnce(responderSign) sinon.assert.calledWithExactly(responderSign, sinon.match('deposit_ack'), sinon.match.string) + sinon.assert.calledOnce(sign) + sinon.assert.calledWithExactly(sign, sinon.match.string) + const { txType, tx } = unpackTx(sign.firstCall.args[0]) + txType.should.equal('channelDeposit') + tx.should.eql({ + ...tx, + fromId: await initiator.address(), + amount: amount.toString() + }) }) it('can request a deposit and reject', async () => { + const sign = sinon.spy(initiator.signTransaction.bind(initiator)) + const amount = 2 const onOnChainTx = sinon.spy() const onOwnDepositLocked = sinon.spy() const onDepositLocked = sinon.spy() responderShouldRejectUpdate = true const result = await initiatorCh.deposit( - 2, - async (tx) => initiator.signTransaction(tx), + amount, + sign, { onOnChainTx, onOwnDepositLocked, onDepositLocked } ) result.should.eql({ accepted: false }) @@ -245,14 +324,31 @@ describe('Channel', function () { sinon.assert.notCalled(initiatorSign) sinon.assert.calledOnce(responderSign) sinon.assert.calledWithExactly(responderSign, sinon.match('deposit_ack'), sinon.match.string) + const { txType, tx } = unpackTx(sign.firstCall.args[0]) + txType.should.equal('channelDeposit') + tx.should.eql({ + ...tx, + fromId: await initiator.address(), + amount: amount.toString() + }) }) it('can close a channel', async () => { - const tx = await initiatorCh.shutdown(async (tx) => await initiator.signTransaction(tx)) - tx.should.be.a('string') + const sign = sinon.spy(initiator.signTransaction.bind(initiator)) + const result = await initiatorCh.shutdown(sign) + result.should.be.a('string') sinon.assert.notCalled(initiatorSign) sinon.assert.calledOnce(responderSign) sinon.assert.calledWithExactly(responderSign, sinon.match('shutdown_sign_ack'), sinon.match.string) + sinon.assert.calledOnce(sign) + sinon.assert.calledWithExactly(sign, sinon.match.string) + const { txType, tx } = unpackTx(sign.firstCall.args[0]) + txType.should.equal('channelCloseMutual') + tx.should.eql({ + ...tx, + fromId: await initiator.address(), + // TODO: check `initiatorAmountFinal` and `responderAmountFinal` + }) }) it('can leave a channel', async () => { From dbb8fa5a9a85820b9e6c750be2768f01aee4c7d3 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Tue, 12 Mar 2019 20:53:28 +0000 Subject: [PATCH 07/21] Add channel close solo and settle tx serialization --- es/channel/handlers.js | 4 ++- es/channel/index.js | 13 ++++++++- es/channel/internal.js | 4 ++- es/tx/builder/schema.js | 16 ++++++++++ es/tx/tx.js | 58 +++++++++++++++++++++++++++++++++++++ test/integration/channel.js | 48 ++++++++++++++++++++++++++++-- 6 files changed, 138 insertions(+), 5 deletions(-) diff --git a/es/channel/handlers.js b/es/channel/handlers.js index 629a47dd80..1983ffc3bb 100644 --- a/es/channel/handlers.js +++ b/es/channel/handlers.js @@ -21,7 +21,8 @@ import { changeStatus, changeState, send, - emit + emit, + channelId } from './internal' import * as R from 'ramda' @@ -86,6 +87,7 @@ export function awaitingBlockInclusion (channel, message, state) { export function awaitingOpenConfirmation (channel, message, state) { if (message.method === 'channels.info' && message.params.data.event === 'open') { + channelId.set(channel, message.params.channel_id) return { handler: awaitingInitialState } } } diff --git a/es/channel/index.js b/es/channel/index.js index 80fdf79340..28edd2aa74 100644 --- a/es/channel/index.js +++ b/es/channel/index.js @@ -31,7 +31,8 @@ import { initialize, enqueueAction, send, - messageId + messageId, + channelId } from './internal' /** @@ -62,6 +63,15 @@ function state () { return channelState.get(this) } +/** + * Get channel id + * + * @return {string} + */ +function id () { + return channelId.get(this) +} + /** * Trigger an update * @@ -396,6 +406,7 @@ const Channel = AsyncInit.compose({ on, status, state, + id, update, poi, balances, diff --git a/es/channel/internal.js b/es/channel/internal.js index c5dcc9a557..ffc151869b 100644 --- a/es/channel/internal.js +++ b/es/channel/internal.js @@ -32,6 +32,7 @@ const messageQueueLocked = new WeakMap() const actionQueue = new WeakMap() const actionQueueLocked = new WeakMap() const sequence = new WeakMap() +const channelId = new WeakMap() function channelURL (url, { endpoint = 'channel', ...params }) { const paramString = R.join('&', R.values(R.mapObjIndexed((value, key) => @@ -185,5 +186,6 @@ export { changeState, send, enqueueAction, - messageId + messageId, + channelId } diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index ed992d646a..2aa979a787 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -42,6 +42,7 @@ const OBJECT_TAG_CHANNEL_CREATE_TX = 50 const OBJECT_TAG_CHANNEL_DEPOSIT_TX = 51 const OBJECT_TAG_CHANNEL_WITHRAW_TX = 52 const OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX = 53 +const OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX = 54 const OBJECT_TAG_CHANNEL_SETTLE_TX = 56 const OBJECT_TAG_CHANNEL_OFFCHAIN_TX = 57 const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX = 570 @@ -106,6 +107,7 @@ export const TX_TYPE = { // STATE CHANNEL channelCreate: 'channelCreate', channelCloseMutual: 'channelCloseMutual', + channelCloseSolo: 'channelCloseSolo', channelDeposit: 'channelDeposit', channelWithdraw: 'channelWithdraw', channelSettle: 'channelSettle', @@ -136,6 +138,7 @@ export const OBJECT_ID_TX_TYPE = { // STATE CHANNEL [OBJECT_TAG_CHANNEL_CREATE_TX]: TX_TYPE.channelCreate, [OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX]: TX_TYPE.channelCloseMutual, + [OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX]: TX_TYPE.channelCloseSolo, [OBJECT_TAG_CHANNEL_DEPOSIT_TX]: TX_TYPE.channelDeposit, [OBJECT_TAG_CHANNEL_WITHRAW_TX]: TX_TYPE.channelWithdraw, [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_TYPE.channelSettle, @@ -429,6 +432,17 @@ const CHANNEL_CLOSE_MUTUAL_TX = [ TX_FIELD('nonce', FIELD_TYPES.int) ] +const CHANNEL_CLOSE_SOLO_TX = [ + ...BASE_TX, + TX_FIELD('channelId', FIELD_TYPES.id, 'ch'), + TX_FIELD('fromId', FIELD_TYPES.id, 'ak'), + TX_FIELD('payload', FIELD_TYPES.string), + TX_FIELD('poi', FIELD_TYPES.binary, 'pi'), + TX_FIELD('ttl', FIELD_TYPES.int), + TX_FIELD('fee', FIELD_TYPES.int), + TX_FIELD('nonce', FIELD_TYPES.int) +] + const CHANNEL_SETTLE_TX = [ ...BASE_TX, TX_FIELD('channelId', FIELD_TYPES.id, 'ch'), @@ -504,6 +518,7 @@ export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.oracleResponse]: TX_SCHEMA_FIELD(ORACLE_RESPOND_TX, OBJECT_TAG_ORACLE_RESPONSE_TRANSACTION), [TX_TYPE.channelCreate]: TX_SCHEMA_FIELD(CHANNEL_CREATE_TX, OBJECT_TAG_CHANNEL_CREATE_TX), [TX_TYPE.channelCloseMutual]: TX_SCHEMA_FIELD(CHANNEL_CLOSE_MUTUAL_TX, OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX), + [TX_TYPE.channelCloseSolo]: TX_SCHEMA_FIELD(CHANNEL_CLOSE_SOLO_TX, OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX), [TX_TYPE.channelDeposit]: TX_SCHEMA_FIELD(CHANNEL_DEPOSIT_TX, OBJECT_TAG_CHANNEL_DEPOSIT_TX), [TX_TYPE.channelWithdraw]: TX_SCHEMA_FIELD(CHANNEL_WITHDRAW_TX, OBJECT_TAG_CHANNEL_WITHRAW_TX), [TX_TYPE.channelSettle]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX), @@ -531,6 +546,7 @@ export const TX_DESERIALIZATION_SCHEMA = { [OBJECT_TAG_ORACLE_RESPONSE_TRANSACTION]: TX_SCHEMA_FIELD(ORACLE_RESPOND_TX, OBJECT_TAG_ORACLE_RESPONSE_TRANSACTION), [OBJECT_TAG_CHANNEL_CREATE_TX]: TX_SCHEMA_FIELD(CHANNEL_CREATE_TX, OBJECT_TAG_CHANNEL_CREATE_TX), [OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX]: TX_SCHEMA_FIELD(CHANNEL_CLOSE_MUTUAL_TX, OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX), + [OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX]: TX_SCHEMA_FIELD(CHANNEL_CLOSE_SOLO_TX, OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX), [OBJECT_TAG_CHANNEL_DEPOSIT_TX]: TX_SCHEMA_FIELD(CHANNEL_DEPOSIT_TX, OBJECT_TAG_CHANNEL_DEPOSIT_TX), [OBJECT_TAG_CHANNEL_WITHRAW_TX]: TX_SCHEMA_FIELD(CHANNEL_WITHDRAW_TX, OBJECT_TAG_CHANNEL_WITHRAW_TX), [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX), diff --git a/es/tx/tx.js b/es/tx/tx.js index 21e03b6c38..be90b80ec4 100644 --- a/es/tx/tx.js +++ b/es/tx/tx.js @@ -246,6 +246,62 @@ async function oracleRespondTx ({ oracleId, callerId, responseTtl, queryId, resp return tx } +async function channelCloseSoloTx ({ channelId, fromId, payload = '', poi }) { + // Calculate fee, get absolute ttl (ttl + height), get account nonce + const { fee, ttl, nonce } = await this.prepareTxParams(TX_TYPE.channelCloseSolo, { senderId: fromId, ...R.head(arguments), payload }) + + // Build transaction using sdk (if nativeMode) or build on `AETERNITY NODE` side + const { tx } = this.nativeMode + ? buildTx(R.merge(R.head(arguments), { + channelId, + fromId, + payload, + poi, + ttl, + fee, + nonce + }), TX_TYPE.channelCloseSolo) + : await this.api.postChannelCloseSolo(R.merge(R.head(arguments), { + channelId, + fromId, + payload, + poi, + ttl, + fee: parseInt(fee), + nonce + })) + + return tx +} + +async function channelSettleTx ({ channelId, fromId, initiatorAmountFinal, responderAmountFinal }) { + // Calculate fee, get absolute ttl (ttl + height), get account nonce + const { fee, ttl, nonce } = await this.prepareTxParams(TX_TYPE.channelSettle, { senderId: fromId, ...R.head(arguments) }) + + // Build transaction using sdk (if nativeMode) or build on `AETERNITY NODE` side + const { tx } = this.nativeMode + ? buildTx(R.merge(R.head(arguments), { + channelId, + fromId, + initiatorAmountFinal, + responderAmountFinal, + ttl, + fee, + nonce + }), TX_TYPE.channelSettle) + : await this.api.postChannelSettle(R.merge(R.head(arguments), { + channelId, + fromId, + initiatorAmountFinal: parseInt(initiatorAmountFinal), + responderAmountFinal: parseInt(responderAmountFinal), + ttl, + fee: parseInt(fee), + nonce + })) + + return tx +} + /** * Compute the absolute ttl by adding the ttl to the current height of the chain * @@ -335,6 +391,8 @@ const Transaction = Node.compose(Tx, { oracleExtendTx, oraclePostQueryTx, oracleRespondTx, + channelCloseSoloTx, + channelSettleTx, getAccountNonce } }) diff --git a/test/integration/channel.js b/test/integration/channel.js index 6411d55837..8e176b73b8 100644 --- a/test/integration/channel.js +++ b/test/integration/channel.js @@ -17,6 +17,7 @@ import { describe, it, before } from 'mocha' import * as sinon from 'sinon' +import { BigNumber } from 'bignumber.js' import { configure, ready, plan, BaseAe, networkId } from './' import { generateKeyPair } from '../../es/utils/crypto' import { unpackTx } from '../../es/tx/builder' @@ -38,7 +39,6 @@ function waitForChannel (channel) { describe('Channel', function () { configure(this) - this.retries(3) let initiator let responder @@ -63,7 +63,7 @@ describe('Channel', function () { ttl: 10000, host: 'localhost', port: 3001, - lockPeriod: 10 + lockPeriod: 1 } before(async function () { @@ -390,7 +390,51 @@ describe('Channel', function () { sinon.assert.notCalled(responderSign) }) + it('can solo close a channel', async () => { + const initiatorAddr = await initiator.address() + const responderAddr = await responder.address() + const poi = await initiatorCh.poi({ + accounts: [initiatorAddr, responderAddr] + }) + const balances = await initiatorCh.balances([initiatorAddr, responderAddr]) + const balanceBeforeClose = await initiator.balance(initiatorAddr) + const closeSoloTx = await initiator.channelCloseSoloTx({ + channelId: await initiatorCh.id(), + fromId: initiatorAddr, + poi + }) + const closeSoloTxFee = unpackTx(closeSoloTx).tx.fee + await initiator.sendTransaction(await initiator.signTransaction(closeSoloTx), { waitMined: true }) + const settleTx = await initiator.channelSettleTx({ + channelId: await initiatorCh.id(), + fromId: initiatorAddr, + initiatorAmountFinal: balances[initiatorAddr], + responderAmountFinal: balances[responderAddr] + }) + const settleTxFee = new unpackTx(settleTx).tx.fee + await initiator.sendTransaction(await initiator.signTransaction(settleTx), { waitMined: true }) + const balanceAfterClose = await initiator.balance(initiatorAddr) + new BigNumber(balanceAfterClose).minus(balanceBeforeClose).plus(closeSoloTxFee).plus(settleTxFee).isEqualTo( + new BigNumber(balances[initiatorAddr]) + ).should.be.equal(true) + await initiatorCh.leave() + }) + describe('throws errors', function () { + before(async function () { + initiatorCh = await Channel({ + ...sharedParams, + role: 'initiator', + sign: initiatorSign + }) + responderCh = await Channel({ + ...sharedParams, + role: 'responder', + sign: responderSign + }) + await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) + }) + async function update ({ from, to, amount, sign }) { return initiatorCh.update( from || await initiator.address(), From ec9a12b0f0fa2874ac006de877aa0a7a1a1fbe29 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Tue, 26 Mar 2019 13:36:39 +0000 Subject: [PATCH 08/21] Add channel slash tx serialization --- es/tx/builder/schema.js | 19 +++++++- es/tx/tx.js | 31 ++++++++++++- test/integration/channel.js | 89 ++++++++++++++++++++++++++++++++++--- 3 files changed, 131 insertions(+), 8 deletions(-) diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index 2aa979a787..4479b50a30 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -43,6 +43,7 @@ const OBJECT_TAG_CHANNEL_DEPOSIT_TX = 51 const OBJECT_TAG_CHANNEL_WITHRAW_TX = 52 const OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX = 53 const OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX = 54 +const OBJECT_TAG_CHANNEL_SLASH_TX = 55 const OBJECT_TAG_CHANNEL_SETTLE_TX = 56 const OBJECT_TAG_CHANNEL_OFFCHAIN_TX = 57 const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX = 570 @@ -108,6 +109,7 @@ export const TX_TYPE = { channelCreate: 'channelCreate', channelCloseMutual: 'channelCloseMutual', channelCloseSolo: 'channelCloseSolo', + channelSlash: 'channelSlash', channelDeposit: 'channelDeposit', channelWithdraw: 'channelWithdraw', channelSettle: 'channelSettle', @@ -120,6 +122,7 @@ export const TX_TYPE = { } export const OBJECT_ID_TX_TYPE = { + [OBJECT_TAG_SIGNED_TRANSACTION]: TX_TYPE.signed, [OBJECT_TAG_SPEND_TRANSACTION]: TX_TYPE.spend, // AENS [OBJECT_TAG_NAME_SERVICE_CLAIM_TRANSACTION]: TX_TYPE.nameClaim, @@ -139,6 +142,7 @@ export const OBJECT_ID_TX_TYPE = { [OBJECT_TAG_CHANNEL_CREATE_TX]: TX_TYPE.channelCreate, [OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX]: TX_TYPE.channelCloseMutual, [OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX]: TX_TYPE.channelCloseSolo, + [OBJECT_TAG_CHANNEL_SLASH_TX]: TX_TYPE.channelSlash, [OBJECT_TAG_CHANNEL_DEPOSIT_TX]: TX_TYPE.channelDeposit, [OBJECT_TAG_CHANNEL_WITHRAW_TX]: TX_TYPE.channelWithdraw, [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_TYPE.channelSettle, @@ -436,7 +440,18 @@ const CHANNEL_CLOSE_SOLO_TX = [ ...BASE_TX, TX_FIELD('channelId', FIELD_TYPES.id, 'ch'), TX_FIELD('fromId', FIELD_TYPES.id, 'ak'), - TX_FIELD('payload', FIELD_TYPES.string), + TX_FIELD('payload', FIELD_TYPES.binary, 'tx'), + TX_FIELD('poi', FIELD_TYPES.binary, 'pi'), + TX_FIELD('ttl', FIELD_TYPES.int), + TX_FIELD('fee', FIELD_TYPES.int), + TX_FIELD('nonce', FIELD_TYPES.int) +] + +const CHANNEL_SLASH_TX = [ + ...BASE_TX, + TX_FIELD('channelId', FIELD_TYPES.id, 'ch'), + TX_FIELD('fromId', FIELD_TYPES.id, 'ak'), + TX_FIELD('payload', FIELD_TYPES.binary, 'tx'), TX_FIELD('poi', FIELD_TYPES.binary, 'pi'), TX_FIELD('ttl', FIELD_TYPES.int), TX_FIELD('fee', FIELD_TYPES.int), @@ -519,6 +534,7 @@ export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.channelCreate]: TX_SCHEMA_FIELD(CHANNEL_CREATE_TX, OBJECT_TAG_CHANNEL_CREATE_TX), [TX_TYPE.channelCloseMutual]: TX_SCHEMA_FIELD(CHANNEL_CLOSE_MUTUAL_TX, OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX), [TX_TYPE.channelCloseSolo]: TX_SCHEMA_FIELD(CHANNEL_CLOSE_SOLO_TX, OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX), + [TX_TYPE.channelSlash]: TX_SCHEMA_FIELD(CHANNEL_SLASH_TX, OBJECT_TAG_CHANNEL_SLASH_TX), [TX_TYPE.channelDeposit]: TX_SCHEMA_FIELD(CHANNEL_DEPOSIT_TX, OBJECT_TAG_CHANNEL_DEPOSIT_TX), [TX_TYPE.channelWithdraw]: TX_SCHEMA_FIELD(CHANNEL_WITHDRAW_TX, OBJECT_TAG_CHANNEL_WITHRAW_TX), [TX_TYPE.channelSettle]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX), @@ -547,6 +563,7 @@ export const TX_DESERIALIZATION_SCHEMA = { [OBJECT_TAG_CHANNEL_CREATE_TX]: TX_SCHEMA_FIELD(CHANNEL_CREATE_TX, OBJECT_TAG_CHANNEL_CREATE_TX), [OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX]: TX_SCHEMA_FIELD(CHANNEL_CLOSE_MUTUAL_TX, OBJECT_TAG_CHANNEL_CLOSE_MUTUAL_TX), [OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX]: TX_SCHEMA_FIELD(CHANNEL_CLOSE_SOLO_TX, OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX), + [OBJECT_TAG_CHANNEL_SLASH_TX]: TX_SCHEMA_FIELD(CHANNEL_SLASH_TX, OBJECT_TAG_CHANNEL_SLASH_TX), [OBJECT_TAG_CHANNEL_DEPOSIT_TX]: TX_SCHEMA_FIELD(CHANNEL_DEPOSIT_TX, OBJECT_TAG_CHANNEL_DEPOSIT_TX), [OBJECT_TAG_CHANNEL_WITHRAW_TX]: TX_SCHEMA_FIELD(CHANNEL_WITHDRAW_TX, OBJECT_TAG_CHANNEL_WITHRAW_TX), [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX), diff --git a/es/tx/tx.js b/es/tx/tx.js index be90b80ec4..9a09c3eb77 100644 --- a/es/tx/tx.js +++ b/es/tx/tx.js @@ -246,7 +246,7 @@ async function oracleRespondTx ({ oracleId, callerId, responseTtl, queryId, resp return tx } -async function channelCloseSoloTx ({ channelId, fromId, payload = '', poi }) { +async function channelCloseSoloTx ({ channelId, fromId, payload, poi }) { // Calculate fee, get absolute ttl (ttl + height), get account nonce const { fee, ttl, nonce } = await this.prepareTxParams(TX_TYPE.channelCloseSolo, { senderId: fromId, ...R.head(arguments), payload }) @@ -274,6 +274,34 @@ async function channelCloseSoloTx ({ channelId, fromId, payload = '', poi }) { return tx } +async function channelSlashTx ({ channelId, fromId, payload, poi }) { + // Calculate fee, get absolute ttl (ttl + height), get account nonce + const { fee, ttl, nonce } = await this.prepareTxParams(TX_TYPE.channelSlash, { senderId: fromId, ...R.head(arguments), payload }) + + // Build transaction using sdk (if nativeMode) or build on `AETERNITY NODE` side + const { tx } = this.nativeMode + ? buildTx(R.merge(R.head(arguments), { + channelId, + fromId, + payload, + poi, + ttl, + fee, + nonce + }), TX_TYPE.channelSlash) + : await this.api.postChannelSlash(R.merge(R.head(arguments), { + channelId, + fromId, + payload, + poi, + ttl, + fee: parseInt(fee), + nonce + })) + + return tx +} + async function channelSettleTx ({ channelId, fromId, initiatorAmountFinal, responderAmountFinal }) { // Calculate fee, get absolute ttl (ttl + height), get account nonce const { fee, ttl, nonce } = await this.prepareTxParams(TX_TYPE.channelSettle, { senderId: fromId, ...R.head(arguments) }) @@ -392,6 +420,7 @@ const Transaction = Node.compose(Tx, { oraclePostQueryTx, oracleRespondTx, channelCloseSoloTx, + channelSlashTx, channelSettleTx, getAccountNonce } diff --git a/test/integration/channel.js b/test/integration/channel.js index 8e176b73b8..fb1d2db4f4 100644 --- a/test/integration/channel.js +++ b/test/integration/channel.js @@ -393,15 +393,23 @@ describe('Channel', function () { it('can solo close a channel', async () => { const initiatorAddr = await initiator.address() const responderAddr = await responder.address() + const { state } = await initiatorCh.update( + await initiator.address(), + await responder.address(), + 100, + tx => initiator.signTransaction(tx) + ) const poi = await initiatorCh.poi({ accounts: [initiatorAddr, responderAddr] }) const balances = await initiatorCh.balances([initiatorAddr, responderAddr]) - const balanceBeforeClose = await initiator.balance(initiatorAddr) + const initiatorBalanceBeforeClose = await initiator.balance(initiatorAddr) + const responderBalanceBeforeClose = await responder.balance(responderAddr) const closeSoloTx = await initiator.channelCloseSoloTx({ channelId: await initiatorCh.id(), fromId: initiatorAddr, - poi + poi, + payload: state }) const closeSoloTxFee = unpackTx(closeSoloTx).tx.fee await initiator.sendTransaction(await initiator.signTransaction(closeSoloTx), { waitMined: true }) @@ -411,13 +419,80 @@ describe('Channel', function () { initiatorAmountFinal: balances[initiatorAddr], responderAmountFinal: balances[responderAddr] }) - const settleTxFee = new unpackTx(settleTx).tx.fee + const settleTxFee = unpackTx(settleTx).tx.fee await initiator.sendTransaction(await initiator.signTransaction(settleTx), { waitMined: true }) - const balanceAfterClose = await initiator.balance(initiatorAddr) - new BigNumber(balanceAfterClose).minus(balanceBeforeClose).plus(closeSoloTxFee).plus(settleTxFee).isEqualTo( + const initiatorBalanceAfterClose = await initiator.balance(initiatorAddr) + const responderBalanceAfterClose = await responder.balance(responderAddr) + new BigNumber(initiatorBalanceAfterClose).minus(initiatorBalanceBeforeClose).plus(closeSoloTxFee).plus(settleTxFee).isEqualTo( new BigNumber(balances[initiatorAddr]) ).should.be.equal(true) - await initiatorCh.leave() + new BigNumber(responderBalanceAfterClose).minus(responderBalanceBeforeClose).isEqualTo( + new BigNumber(balances[responderAddr]) + ).should.be.equal(true) + }) + + it('can dispute via slash tx', async () => { + const initiatorAddr = await initiator.address() + const responderAddr = await responder.address() + initiatorCh = await Channel({ + ...sharedParams, + lockPeriod: 5, + role: 'initiator', + sign: initiatorSign, + port: 3002 + }) + responderCh = await Channel({ + ...sharedParams, + lockPeriod: 5, + role: 'responder', + sign: responderSign, + port: 3002 + }) + await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) + const initiatorBalanceBeforeClose = await initiator.balance(initiatorAddr) + const responderBalanceBeforeClose = await responder.balance(responderAddr) + const oldUpdate = await initiatorCh.update(initiatorAddr, responderAddr, 100, (tx) => initiator.signTransaction(tx)) + const oldPoi = await initiatorCh.poi({ + accounts: [initiatorAddr, responderAddr] + }) + const oldBalances = await initiatorCh.balances([initiatorAddr, responderAddr]) + const recentUpdate = await initiatorCh.update(initiatorAddr, responderAddr, 100, (tx) => initiator.signTransaction(tx)) + const recentPoi = await responderCh.poi({ + accounts: [initiatorAddr, responderAddr] + }) + const recentBalances = await responderCh.balances([initiatorAddr, responderAddr]) + const closeSoloTx = await initiator.channelCloseSoloTx({ + channelId: initiatorCh.id(), + fromId: initiatorAddr, + poi: oldPoi, + payload: oldUpdate.state + }) + const closeSoloTxFee = unpackTx(closeSoloTx).tx.fee + await initiator.sendTransaction(await initiator.signTransaction(closeSoloTx), { waitMined: true }) + const slashTx = await responder.channelSlashTx({ + channelId: responderCh.id(), + fromId: responderAddr, + poi: recentPoi, + payload: recentUpdate.state + }) + const slashTxFee = unpackTx(slashTx).tx.fee + await responder.sendTransaction(await responder.signTransaction(slashTx), { waitMined: true }) + const settleTx = await responder.channelSettleTx({ + channelId: responderCh.id(), + fromId: responderAddr, + initiatorAmountFinal: recentBalances[initiatorAddr], + responderAmountFinal: recentBalances[responderAddr] + }) + const settleTxFee = unpackTx(settleTx).tx.fee + await responder.sendTransaction(await responder.signTransaction(settleTx), { waitMined: true }) + const initiatorBalanceAfterClose = await initiator.balance(initiatorAddr) + const responderBalanceAfterClose = await responder.balance(responderAddr) + new BigNumber(initiatorBalanceAfterClose).minus(initiatorBalanceBeforeClose).plus(closeSoloTxFee).isEqualTo( + new BigNumber(recentBalances[initiatorAddr]) + ).should.be.equal(true) + new BigNumber(responderBalanceAfterClose).minus(responderBalanceBeforeClose).plus(slashTxFee).plus(settleTxFee).isEqualTo( + new BigNumber(recentBalances[responderAddr]) + ).should.be.equal(true) }) describe('throws errors', function () { @@ -425,11 +500,13 @@ describe('Channel', function () { initiatorCh = await Channel({ ...sharedParams, role: 'initiator', + port: 3003, sign: initiatorSign }) responderCh = await Channel({ ...sharedParams, role: 'responder', + port: 3003, sign: responderSign }) await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) From e3bc81193f3ca08f0da818ea2937ae6406edc4a7 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Wed, 27 Mar 2019 12:33:04 +0000 Subject: [PATCH 09/21] Add proof of inclusion tx serialization --- es/tx/builder/index.js | 25 ++++++++++++++++++++++-- es/tx/builder/schema.js | 39 ++++++++++++++++++++++++++++++++----- test/integration/channel.js | 8 ++++++-- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/es/tx/builder/index.js b/es/tx/builder/index.js index 177c47f974..6815ff213c 100644 --- a/es/tx/builder/index.js +++ b/es/tx/builder/index.js @@ -50,6 +50,16 @@ function deserializeField (value, type, prefix) { case FIELD_TYPES.callStack: // TODO: fix this return [readInt(value)] + case FIELD_TYPES.mptree: + return value.map(v => ({ + rootHash: v[0].toString('hex'), + nodes: v[1].map(v => deserializeField(v, FIELD_TYPES.mptreeNode)) + })) + case FIELD_TYPES.mptreeNode: + return { + mptHash: value[0].toString('hex'), + mptValue: value[1] + } default: return value } @@ -69,6 +79,16 @@ function serializeField (value, type, prefix) { return toBytes(value) case FIELD_TYPES.pointers: return buildPointers(value) + case FIELD_TYPES.mptree: + return value.map(v => ([ + Buffer.from(v.rootHash, 'hex'), + v.nodes.map(v => serializeField(v, FIELD_TYPES.mptreeNode)) + ])) + case FIELD_TYPES.mptreeNode: + return [ + Buffer.from(value.mptHash, 'hex'), + value.mptValue + ] default: return value } @@ -255,10 +275,11 @@ export function unpackRawTx (binary, schema) { * @param {String} type Transaction type * @param {Object} [options={}] options * @param {Object} [options.excludeKeys] excludeKeys Array of keys to exclude for validation and build + * @param {String} [options.prefix] Prefix of transaction * @throws {Error} Validation error * @return {Object} { tx, rlpEncoded, binary } Object with tx -> Base64Check transaction hash with 'tx_' prefix, rlp encoded transaction and binary transaction */ -export function buildTx (params, type, { excludeKeys = [] } = {}) { +export function buildTx (params, type, { excludeKeys = [], prefix = 'tx' } = {}) { if (!TX_SERIALIZATION_SCHEMA[type]) { throw new Error('Transaction serialization not implemented for ' + type) } @@ -266,7 +287,7 @@ export function buildTx (params, type, { excludeKeys = [] } = {}) { const binary = buildRawTx({ ...params, VSN, tag }, schema, { excludeKeys }).filter(e => e !== undefined) const rlpEncoded = rlp.encode(binary) - const tx = encode(rlpEncoded, 'tx') + const tx = encode(rlpEncoded, prefix) return { tx, rlpEncoded, binary, txObject: unpackRawTx(binary, schema) } } diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index 4479b50a30..6017d77d33 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -24,6 +24,7 @@ const ID_TAG_CHANNEL = 6 // # OBJECT tags // # see https://github.com/aeternity/protocol/blob/master/serializations.md#binary-serialization +const OBJECT_TAG_ACCOUNT = 10 export const OBJECT_TAG_SIGNED_TRANSACTION = 11 const OBJECT_TAG_SPEND_TRANSACTION = 12 const OBJECT_TAG_ORACLE_REGISTER_TRANSACTION = 22 @@ -51,6 +52,7 @@ const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX = 571 const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX = 572 const OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX = 573 const OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX = 574 +const OBJECT_TAG_PROOF_OF_INCLUSION = 60 const TX_FIELD = (name, type, prefix) => [name, type, prefix] const TX_SCHEMA_FIELD = (schema, objectId) => [schema, objectId] @@ -89,6 +91,7 @@ const ABI_VERSIONS = { * @property {String} oracleResponse */ export const TX_TYPE = { + account: 'account', signed: 'signedTx', spend: 'spendTx', // AENS @@ -118,10 +121,12 @@ export const TX_TYPE = { channelOffChainUpdateDeposit: 'channelOffChainUpdateDeposit', channelOffChainUpdateWithdrawal: 'channelOffChainUpdateWithdrawal', channelOffChainCreateContract: 'channelOffChainCreateContract', - channelOffChainCallContract: 'channelOffChainCallContract' + channelOffChainCallContract: 'channelOffChainCallContract', + proofOfInclusion: 'proofOfInclusion' } export const OBJECT_ID_TX_TYPE = { + [OBJECT_TAG_ACCOUNT]: TX_TYPE.account, [OBJECT_TAG_SIGNED_TRANSACTION]: TX_TYPE.signed, [OBJECT_TAG_SPEND_TRANSACTION]: TX_TYPE.spend, // AENS @@ -151,7 +156,8 @@ export const OBJECT_ID_TX_TYPE = { [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX]: TX_TYPE.channelOffChainUpdateDeposit, [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX]: TX_TYPE.channelOffChainUpdateWithdrawal, [OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX]: TX_TYPE.channelOffChainCreateContract, - [OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX]: TX_TYPE.channelOffChainCallContract + [OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX]: TX_TYPE.channelOffChainCallContract, + [OBJECT_TAG_PROOF_OF_INCLUSION]: TX_TYPE.proofOfInclusion } export const FIELD_TYPES = { @@ -163,7 +169,10 @@ export const FIELD_TYPES = { signatures: 'signatures', pointers: 'pointers', offChainUpdates: 'offChainUpdates', - callStack: 'callStack' + callStack: 'callStack', + proofOfInclusion: 'proofOfInclusion', + mptree: 'mptree', + mptreeNode: 'mptreeNode' } // FEE CALCULATION @@ -240,6 +249,12 @@ const BASE_TX = [ TX_FIELD('VSN', FIELD_TYPES.int) ] +const ACCOUNT_TX = [ + ...BASE_TX, + TX_FIELD('nonce', FIELD_TYPES.int), + TX_FIELD('balance', FIELD_TYPES.int) +] + const SPEND_TX = [ ...BASE_TX, TX_FIELD('senderId', FIELD_TYPES.id, 'ak'), @@ -517,7 +532,18 @@ const CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX = [ TX_FIELD('amount', FIELD_TYPES.int) ] +const PROOF_OF_INCLUSION_TX = [ + ...BASE_TX, + TX_FIELD('accounts', FIELD_TYPES.mptree), + TX_FIELD('calls', FIELD_TYPES.mptree), + TX_FIELD('channels', FIELD_TYPES.mptree), + TX_FIELD('contracts', FIELD_TYPES.mptree), + TX_FIELD('ns', FIELD_TYPES.mptree), + TX_FIELD('oracles', FIELD_TYPES.mptree) +] + export const TX_SERIALIZATION_SCHEMA = { + [TX_TYPE.account]: TX_SCHEMA_FIELD(ACCOUNT_TX, OBJECT_TAG_ACCOUNT), [TX_TYPE.signed]: TX_SCHEMA_FIELD(SIGNED_TX, OBJECT_TAG_SIGNED_TRANSACTION), [TX_TYPE.spend]: TX_SCHEMA_FIELD(SPEND_TX, OBJECT_TAG_SPEND_TRANSACTION), [TX_TYPE.namePreClaim]: TX_SCHEMA_FIELD(NAME_PRE_CLAIM_TX, OBJECT_TAG_NAME_SERVICE_PRECLAIM_TRANSACTION), @@ -543,10 +569,12 @@ export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.channelOffChainUpdateDeposit]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX), [TX_TYPE.channelOffChainUpdateWithdrawal]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX), [TX_TYPE.channelOffChainCreateContract]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX), - [TX_TYPE.channelOffChainCallContract]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CALL_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX) + [TX_TYPE.channelOffChainCallContract]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CALL_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX), + [TX_TYPE.proofOfInclusion]: TX_SCHEMA_FIELD(PROOF_OF_INCLUSION_TX, OBJECT_TAG_PROOF_OF_INCLUSION) } export const TX_DESERIALIZATION_SCHEMA = { + [OBJECT_TAG_ACCOUNT]: TX_SCHEMA_FIELD(ACCOUNT_TX, OBJECT_TAG_ACCOUNT), [OBJECT_TAG_SIGNED_TRANSACTION]: TX_SCHEMA_FIELD(SIGNED_TX, OBJECT_TAG_SIGNED_TRANSACTION), [OBJECT_TAG_SPEND_TRANSACTION]: TX_SCHEMA_FIELD(SPEND_TX, OBJECT_TAG_SPEND_TRANSACTION), [OBJECT_TAG_NAME_SERVICE_PRECLAIM_TRANSACTION]: TX_SCHEMA_FIELD(NAME_PRE_CLAIM_TX, OBJECT_TAG_NAME_SERVICE_PRECLAIM_TRANSACTION), @@ -572,7 +600,8 @@ export const TX_DESERIALIZATION_SCHEMA = { [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX), - [OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CALL_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX) + [OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CALL_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX), + [OBJECT_TAG_PROOF_OF_INCLUSION]: TX_SCHEMA_FIELD(PROOF_OF_INCLUSION_TX, OBJECT_TAG_PROOF_OF_INCLUSION) } // VERIFICATION SCHEMA diff --git a/test/integration/channel.js b/test/integration/channel.js index fb1d2db4f4..5e7da53ad4 100644 --- a/test/integration/channel.js +++ b/test/integration/channel.js @@ -20,7 +20,8 @@ import * as sinon from 'sinon' import { BigNumber } from 'bignumber.js' import { configure, ready, plan, BaseAe, networkId } from './' import { generateKeyPair } from '../../es/utils/crypto' -import { unpackTx } from '../../es/tx/builder' +import { unpackTx, buildTx } from '../../es/tx/builder' +import { decode } from '../../es/tx/builder/helpers' import Channel from '../../es/channel' const wsUrl = process.env.WS_URL || 'ws://node:3014' @@ -181,7 +182,10 @@ describe('Channel', function () { const responderPoi = await responderCh.poi(params) initiatorPoi.should.be.a('string') responderPoi.should.be.a('string') - // TODO: proof of inclusion deserialization + const unpackedInitiatorPoi = unpackTx(decode(initiatorPoi, 'pi'), true) + const unpackedResponderPoi = unpackTx(decode(responderPoi, 'pi'), true) + buildTx(unpackedInitiatorPoi.tx, unpackedInitiatorPoi.txType, { prefix: 'pi' }).tx.should.equal(initiatorPoi) + buildTx(unpackedResponderPoi.tx, unpackedResponderPoi.txType, { prefix: 'pi' }).tx.should.equal(responderPoi) }) it('can get balances', async () => { From 83b249528162c36a847175a47543f290653ee04e Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Thu, 28 Mar 2019 13:24:00 +0000 Subject: [PATCH 10/21] Add basic merkle patricia tree implementation --- es/tx/builder/index.js | 21 ++-------- es/tx/builder/schema.js | 3 +- es/utils/mptree.js | 92 +++++++++++++++++++++++++++++++++++++++++ test/unit/mptree.js | 48 +++++++++++++++++++++ 4 files changed, 144 insertions(+), 20 deletions(-) create mode 100644 es/utils/mptree.js create mode 100644 test/unit/mptree.js diff --git a/es/tx/builder/index.js b/es/tx/builder/index.js index 6815ff213c..61ac57ade6 100644 --- a/es/tx/builder/index.js +++ b/es/tx/builder/index.js @@ -16,6 +16,7 @@ import { } from './schema' import { readInt, readId, readPointers, writeId, writeInt, buildPointers, encode, decode } from './helpers' import { toBytes } from '../../utils/bytes' +import * as mpt from '../../utils/mptree' /** * JavaScript-based Transaction builder @@ -51,15 +52,7 @@ function deserializeField (value, type, prefix) { // TODO: fix this return [readInt(value)] case FIELD_TYPES.mptree: - return value.map(v => ({ - rootHash: v[0].toString('hex'), - nodes: v[1].map(v => deserializeField(v, FIELD_TYPES.mptreeNode)) - })) - case FIELD_TYPES.mptreeNode: - return { - mptHash: value[0].toString('hex'), - mptValue: value[1] - } + return value.map(mpt.deserialize) default: return value } @@ -80,15 +73,7 @@ function serializeField (value, type, prefix) { case FIELD_TYPES.pointers: return buildPointers(value) case FIELD_TYPES.mptree: - return value.map(v => ([ - Buffer.from(v.rootHash, 'hex'), - v.nodes.map(v => serializeField(v, FIELD_TYPES.mptreeNode)) - ])) - case FIELD_TYPES.mptreeNode: - return [ - Buffer.from(value.mptHash, 'hex'), - value.mptValue - ] + return value.map(mpt.serialize) default: return value } diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index 6017d77d33..38f4c4237c 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -171,8 +171,7 @@ export const FIELD_TYPES = { offChainUpdates: 'offChainUpdates', callStack: 'callStack', proofOfInclusion: 'proofOfInclusion', - mptree: 'mptree', - mptreeNode: 'mptreeNode' + mptree: 'mptree' } // FEE CALCULATION diff --git a/es/utils/mptree.js b/es/utils/mptree.js new file mode 100644 index 0000000000..664454b52e --- /dev/null +++ b/es/utils/mptree.js @@ -0,0 +1,92 @@ +/* + * ISC License (ISC) + * Copyright (c) 2018 aeternity developers + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +const NODE_TYPES = { + branch: 1, + extension: 2, + leaf: 3 +} + +function nodeType (node, remainingPath) { + if (node.length === 17) { + return NODE_TYPES.branch + } + if (node.length === 2) { + const isOdd = remainingPath.length % 2 + const nibble = node[0].toString('hex')[0] + if ((!isOdd && nibble === '0') || (isOdd && nibble === '1')) { + return NODE_TYPES.extension + } + if ((!isOdd && nibble === '2') || (isOdd && nibble === '3')) { + return NODE_TYPES.leaf + } + } +} + +/** + * Deserialize Merkle Patricia Tree + * @rtype (binary: Array) => Object + * @param {Array} binary - Binary + * @return {Object} Merkle Patricia Tree + */ +export function deserialize (binary) { + return { + rootHash: binary[0].toString('hex'), + nodes: binary[1].reduce((prev, node) => ({ + ...prev, + [node[0].toString('hex')]: node[1] + }), {}) + } +} + +/** + * Serialize Merkle Patricia Tree + * @rtype (tree: Object) => Array + * @param {Object} tree - Merkle Patricia Tree + * @return {Array} Binary + */ +export function serialize (tree) { + return [ + Buffer.from(tree.rootHash, 'hex'), + Object.entries(tree.nodes).map(([mptHash, value]) => ([ + Buffer.from(mptHash, 'hex'), + value + ])) + ] +} + +/** + * Retrieve value from Merkle Patricia Tree + * @rtype (tree: Object, key: String) => Buffer + * @param {Object} tree - Merkle Patricia Tree + * @param {String} key - The key of the element to retrieve + * @return {Buffer} Value associated to the specified key + */ +export function get (tree, key, hash) { + const node = hash ? tree.nodes[hash] : tree.nodes[tree.rootHash] + const type = nodeType(node, key) + if (type === NODE_TYPES.branch) { + const nextHash = node[parseInt(key[0], 16)].toString('hex') + return get(tree, key.substr(1), nextHash) + } + // TODO: handle NODE_TYPES.extension + if (type === NODE_TYPES.leaf) { + if (node[0].toString('hex').substr(1) === key) { + return node[1] + } + } +} diff --git a/test/unit/mptree.js b/test/unit/mptree.js new file mode 100644 index 0000000000..c88c01402f --- /dev/null +++ b/test/unit/mptree.js @@ -0,0 +1,48 @@ +/* + * ISC License (ISC) + * Copyright (c) 2018 aeternity developers + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +import '../' +import { describe, it } from 'mocha' +import { rlp } from '../../es/utils/crypto' +import { serialize, deserialize, get } from '../../es/utils/mptree' + +describe('Merkle Patricia Tree', function () { + const binary = Buffer.from('f9013ea0d4b40fbf270d982d9c9bebc8acd6711db9a2465459f1cb67450f495e3a78f5d2f9011af850a0056232c6f764553f472dacd7bba764e4d630adce971e4437dcf07421e20d6cf3eea03e2e29b62366a6b1e363ebf174fce8e4d9ad61abdc2dde65e3f74923dcd629c48ccb0a010087038d7ea4c67ffcf850a065657db43209ef7d57acb7aaf2e2c38f8828f9d425e4bec0d7de5bfa26496c61eea03269a8e17fffe495df7b47bf0ffb94897e1060baf3192e99978d91010325b62d8ccb0a010087038d7ea4c68004f874a0d4b40fbf270d982d9c9bebc8acd6711db9a2465459f1cb67450f495e3a78f5d2f85180a065657db43209ef7d57acb7aaf2e2c38f8828f9d425e4bec0d7de5bfa26496c618080a0056232c6f764553f472dacd7bba764e4d630adce971e4437dcf07421e20d6cf3808080808080808080808080', 'hex') + const map = { + '4e2e29b62366a6b1e363ebf174fce8e4d9ad61abdc2dde65e3f74923dcd629c4': 'cb0a010087038d7ea4c67ffc', + '1269a8e17fffe495df7b47bf0ffb94897e1060baf3192e99978d91010325b62d': 'cb0a010087038d7ea4c68004' + } + + it('can deserialize', () => { + const tree = deserialize(rlp.decode(binary)) + tree.should.be.an('object') + tree.rootHash.should.be.a('string') + tree.nodes.should.be.an('object') + }) + + it('can serialize', () => { + const serialized = rlp.encode(serialize(deserialize(rlp.decode(binary)))) + serialized.toString('hex').should.equal(binary.toString('hex')) + }) + + it('can retrieve values', () => { + const tree = deserialize(rlp.decode(binary)) + Object.entries(map).forEach(([key, value]) => { + get(tree, key).toString('hex').should.equal(value) + }) + }) +}) From 6dab14259d66c750ea4c6ab8124ed020930d7b64 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Fri, 29 Mar 2019 18:09:49 +0000 Subject: [PATCH 11/21] Add merkle patricia tree serialization and verify function --- es/tx/builder/helpers.js | 2 +- es/tx/builder/index.js | 14 ++++++ es/tx/builder/schema.js | 100 ++++++++++++++++++++++++++++++++++----- es/utils/mptree.js | 70 +++++++++++++++++++++++---- test/unit/mptree.js | 9 +++- 5 files changed, 173 insertions(+), 22 deletions(-) diff --git a/es/tx/builder/helpers.js b/es/tx/builder/helpers.js index 9c25d188cd..0830705700 100644 --- a/es/tx/builder/helpers.js +++ b/es/tx/builder/helpers.js @@ -20,7 +20,7 @@ import { BigNumber } from 'bignumber.js' export const createSalt = salt -const base64Types = ['tx', 'st', 'ss', 'pi', 'ov', 'or', 'cb'] +const base64Types = ['tx', 'st', 'ss', 'pi', 'ov', 'or', 'cb', 'cs'] /** * Build a contract public key diff --git a/es/tx/builder/index.js b/es/tx/builder/index.js index 61ac57ade6..42d23bb301 100644 --- a/es/tx/builder/index.js +++ b/es/tx/builder/index.js @@ -38,6 +38,10 @@ function deserializeField (value, type, prefix) { return readInt(value) case FIELD_TYPES.id: return readId(value) + case FIELD_TYPES.ids: + return value.map(readId) + case FIELD_TYPES.bool: + return value[0] === 1 case FIELD_TYPES.binary: return encode(value, prefix) case FIELD_TYPES.string: @@ -46,6 +50,12 @@ function deserializeField (value, type, prefix) { return readPointers(value) case FIELD_TYPES.rlpBinary: return unpackTx(value, true) + case FIELD_TYPES.rlpBinaries: + return value.map(v => unpackTx(v, true)) + case FIELD_TYPES.rawBinary: + return value + case FIELD_TYPES.hex: + return value.toString('hex') case FIELD_TYPES.offChainUpdates: return value.map(v => unpackTx(v, true)) case FIELD_TYPES.callStack: @@ -64,6 +74,10 @@ function serializeField (value, type, prefix) { return writeInt(value) case FIELD_TYPES.id: return writeId(value) + case FIELD_TYPES.ids: + return value.map(writeId) + case FIELD_TYPES.bool: + return Buffer.from([value ? 1 : 0]) case FIELD_TYPES.binary: return decode(value, prefix) case FIELD_TYPES.signatures: diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index 38f4c4237c..cbdd6310fa 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -36,9 +36,9 @@ const OBJECT_TAG_NAME_SERVICE_PRECLAIM_TRANSACTION = 33 const OBJECT_TAG_NAME_SERVICE_UPDATE_TRANSACTION = 34 const OBJECT_TAG_NAME_SERVICE_REVOKE_TRANSACTION = 35 const OBJECT_TAG_NAME_SERVICE_TRANSFER_TRANSACTION = 36 +const OBJECT_TAG_CONTRACT = 40 const OBJECT_TAG_CONTRACT_CREATE_TRANSACTION = 42 const OBJECT_TAG_CONTRACT_CALL_TRANSACTION = 43 - const OBJECT_TAG_CHANNEL_CREATE_TX = 50 const OBJECT_TAG_CHANNEL_DEPOSIT_TX = 51 const OBJECT_TAG_CHANNEL_WITHRAW_TX = 52 @@ -53,6 +53,11 @@ const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX = 572 const OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX = 573 const OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX = 574 const OBJECT_TAG_PROOF_OF_INCLUSION = 60 +const OBJECT_TAG_STATE_TREES = 62 +const OBJECT_TAG_MERKLE_PATRICIA_TREE = 63 +const OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE = 64 +const OBJECT_TAG_CONTRACTS_TREE = 621 +const OBJECT_TAG_CONTRACT_CALLS_TREE = 622 const TX_FIELD = (name, type, prefix) => [name, type, prefix] const TX_SCHEMA_FIELD = (schema, objectId) => [schema, objectId] @@ -101,6 +106,7 @@ export const TX_TYPE = { nameRevoke: 'nameRevokeTx', nameTransfer: 'nameTransfer', // CONTRACT + contract: 'contract', contractCreate: 'contractCreateTx', contractCall: 'contractCallTx', // ORACLE @@ -122,7 +128,12 @@ export const TX_TYPE = { channelOffChainUpdateWithdrawal: 'channelOffChainUpdateWithdrawal', channelOffChainCreateContract: 'channelOffChainCreateContract', channelOffChainCallContract: 'channelOffChainCallContract', - proofOfInclusion: 'proofOfInclusion' + proofOfInclusion: 'proofOfInclusion', + stateTrees: 'stateTrees', + merklePatriciaTree: 'merklePatriciaTree', + merklePatriciaTreeValue: 'merklePatriciaTreeValue', + contractsTree: 'contractsTree', + contractCallsTree: 'contractCallsTree' } export const OBJECT_ID_TX_TYPE = { @@ -136,6 +147,7 @@ export const OBJECT_ID_TX_TYPE = { [OBJECT_TAG_NAME_SERVICE_REVOKE_TRANSACTION]: TX_TYPE.nameRevoke, [OBJECT_TAG_NAME_SERVICE_TRANSFER_TRANSACTION]: TX_TYPE.nameTransfer, // CONTRACT + [OBJECT_TAG_CONTRACT]: TX_TYPE.contract, [OBJECT_TAG_CONTRACT_CREATE_TRANSACTION]: TX_TYPE.contractCreate, [OBJECT_TAG_CONTRACT_CALL_TRANSACTION]: TX_TYPE.contractCall, // ORACLE @@ -157,21 +169,31 @@ export const OBJECT_ID_TX_TYPE = { [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX]: TX_TYPE.channelOffChainUpdateWithdrawal, [OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX]: TX_TYPE.channelOffChainCreateContract, [OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX]: TX_TYPE.channelOffChainCallContract, - [OBJECT_TAG_PROOF_OF_INCLUSION]: TX_TYPE.proofOfInclusion + [OBJECT_TAG_PROOF_OF_INCLUSION]: TX_TYPE.proofOfInclusion, + [OBJECT_TAG_STATE_TREES]: TX_TYPE.stateTrees, + [OBJECT_TAG_MERKLE_PATRICIA_TREE]: TX_TYPE.merklePatriciaTree, + [OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE]: TX_TYPE.merklePatriciaTreeValue, + [OBJECT_TAG_CONTRACTS_TREE]: TX_TYPE.contractsTree, + [OBJECT_TAG_CONTRACT_CALLS_TREE]: TX_TYPE.contractCallsTree, } export const FIELD_TYPES = { int: 'int', id: 'id', + ids: 'ids', string: 'string', binary: 'binary', rlpBinary: 'rlpBinary', + rlpBinaries: 'rlpBinaries', + rawBinary: 'rawBinary', + bool: 'bool', + hex: 'hex', signatures: 'signatures', pointers: 'pointers', offChainUpdates: 'offChainUpdates', callStack: 'callStack', proofOfInclusion: 'proofOfInclusion', - mptree: 'mptree' + mptree: 'mptree', } // FEE CALCULATION @@ -321,6 +343,17 @@ const NAME_REVOKE_TX = [ TX_FIELD('ttl', FIELD_TYPES.int) ] +const CONTRACT_TX = [ + ...BASE_TX, + TX_FIELD('owner', FIELD_TYPES.id, 'ak'), + TX_FIELD('ctVersion', FIELD_TYPES.int), + TX_FIELD('code', FIELD_TYPES.binary, 'cb'), + TX_FIELD('log', FIELD_TYPES.binary, 'cb'), + TX_FIELD('active', FIELD_TYPES.bool), + TX_FIELD('referers', FIELD_TYPES.ids, 'ak'), + TX_FIELD('deposit', FIELD_TYPES.int) +] + const CONTRACT_CREATE_TX = [ ...BASE_TX, TX_FIELD('ownerId', FIELD_TYPES.id, 'ak'), @@ -533,12 +566,43 @@ const CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX = [ const PROOF_OF_INCLUSION_TX = [ ...BASE_TX, - TX_FIELD('accounts', FIELD_TYPES.mptree), - TX_FIELD('calls', FIELD_TYPES.mptree), - TX_FIELD('channels', FIELD_TYPES.mptree), - TX_FIELD('contracts', FIELD_TYPES.mptree), - TX_FIELD('ns', FIELD_TYPES.mptree), - TX_FIELD('oracles', FIELD_TYPES.mptree) + TX_FIELD('accounts', FIELD_TYPES.mptrees), + TX_FIELD('calls', FIELD_TYPES.mptrees), + TX_FIELD('channels', FIELD_TYPES.mptrees), + TX_FIELD('contracts', FIELD_TYPES.mptrees), + TX_FIELD('ns', FIELD_TYPES.mptrees), + TX_FIELD('oracles', FIELD_TYPES.mptrees) +] + +const STATE_TREES_TX = [ + ...BASE_TX, + TX_FIELD('contracts', FIELD_TYPES.rlpBinary), + TX_FIELD('calls', FIELD_TYPES.rlpBinary), + TX_FIELD('channels', FIELD_TYPES.rlpBinary), + TX_FIELD('ns', FIELD_TYPES.rlpBinary), + TX_FIELD('oracles', FIELD_TYPES.rlpBinary), + TX_FIELD('accounts', FIELD_TYPES.rlpBinary) +] + +const MERKLE_PATRICIA_TREE_TX = [ + ...BASE_TX, + TX_FIELD('values', FIELD_TYPES.rlpBinaries) +] + +const MERKLE_PATRICIA_TREE_VALUE_TX = [ + ...BASE_TX, + TX_FIELD('key', FIELD_TYPES.hex), + TX_FIELD('value', FIELD_TYPES.rawBinary) +] + +const CONTRACTS_TREE_TX = [ + ...BASE_TX, + TX_FIELD('contracts', FIELD_TYPES.rlpBinary) +] + +const CONTRACT_CALLS_TREE_TX = [ + ...BASE_TX, + TX_FIELD('calls', FIELD_TYPES.rlpBinary) ] export const TX_SERIALIZATION_SCHEMA = { @@ -550,6 +614,7 @@ export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.nameUpdate]: TX_SCHEMA_FIELD(NAME_UPDATE_TX, OBJECT_TAG_NAME_SERVICE_UPDATE_TRANSACTION), [TX_TYPE.nameTransfer]: TX_SCHEMA_FIELD(NAME_TRANSFER_TX, OBJECT_TAG_NAME_SERVICE_TRANSFER_TRANSACTION), [TX_TYPE.nameRevoke]: TX_SCHEMA_FIELD(NAME_REVOKE_TX, OBJECT_TAG_NAME_SERVICE_REVOKE_TRANSACTION), + [TX_TYPE.contract]: TX_SCHEMA_FIELD(CONTRACT_TX, OBJECT_TAG_CONTRACT), [TX_TYPE.contractCreate]: TX_SCHEMA_FIELD(CONTRACT_CREATE_TX, OBJECT_TAG_CONTRACT_CREATE_TRANSACTION), [TX_TYPE.contractCall]: TX_SCHEMA_FIELD(CONTRACT_CALL_TX, OBJECT_TAG_CONTRACT_CALL_TRANSACTION), [TX_TYPE.oracleRegister]: TX_SCHEMA_FIELD(ORACLE_REGISTER_TX, OBJECT_TAG_ORACLE_REGISTER_TRANSACTION), @@ -569,7 +634,12 @@ export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.channelOffChainUpdateWithdrawal]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX), [TX_TYPE.channelOffChainCreateContract]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX), [TX_TYPE.channelOffChainCallContract]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CALL_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX), - [TX_TYPE.proofOfInclusion]: TX_SCHEMA_FIELD(PROOF_OF_INCLUSION_TX, OBJECT_TAG_PROOF_OF_INCLUSION) + [TX_TYPE.proofOfInclusion]: TX_SCHEMA_FIELD(PROOF_OF_INCLUSION_TX, OBJECT_TAG_PROOF_OF_INCLUSION), + [TX_TYPE.stateTrees]: TX_SCHEMA_FIELD(STATE_TREES_TX, OBJECT_TAG_STATE_TREES), + [TX_TYPE.merklePatriciaTree]: TX_SCHEMA_FIELD(MERKLE_PATRICIA_TREE_TX, OBJECT_TAG_MERKLE_PATRICIA_TREE), + [TX_TYPE.merklePatriciaTreeValue]: TX_SCHEMA_FIELD(MERKLE_PATRICIA_TREE_VALUE_TX, OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE), + [TX_TYPE.contractsTree]: TX_SCHEMA_FIELD(CONTRACTS_TREE_TX, OBJECT_TAG_CONTRACTS_TREE), + [TX_TYPE.contractCallsTree]: TX_SCHEMA_FIELD(CONTRACT_CALLS_TREE_TX, OBJECT_TAG_CONTRACT_CALLS_TREE) } export const TX_DESERIALIZATION_SCHEMA = { @@ -581,6 +651,7 @@ export const TX_DESERIALIZATION_SCHEMA = { [OBJECT_TAG_NAME_SERVICE_UPDATE_TRANSACTION]: TX_SCHEMA_FIELD(NAME_UPDATE_TX, OBJECT_TAG_NAME_SERVICE_UPDATE_TRANSACTION), [OBJECT_TAG_NAME_SERVICE_TRANSFER_TRANSACTION]: TX_SCHEMA_FIELD(NAME_TRANSFER_TX, OBJECT_TAG_NAME_SERVICE_TRANSFER_TRANSACTION), [OBJECT_TAG_NAME_SERVICE_REVOKE_TRANSACTION]: TX_SCHEMA_FIELD(NAME_REVOKE_TX, OBJECT_TAG_NAME_SERVICE_REVOKE_TRANSACTION), + [OBJECT_TAG_CONTRACT]: TX_SCHEMA_FIELD(CONTRACT_TX, OBJECT_TAG_CONTRACT), [OBJECT_TAG_CONTRACT_CREATE_TRANSACTION]: TX_SCHEMA_FIELD(CONTRACT_CREATE_TX, OBJECT_TAG_CONTRACT_CREATE_TRANSACTION), [OBJECT_TAG_CONTRACT_CALL_TRANSACTION]: TX_SCHEMA_FIELD(CONTRACT_CALL_TX, OBJECT_TAG_CONTRACT_CALL_TRANSACTION), [OBJECT_TAG_ORACLE_REGISTER_TRANSACTION]: TX_SCHEMA_FIELD(ORACLE_REGISTER_TX, OBJECT_TAG_ORACLE_REGISTER_TRANSACTION), @@ -600,7 +671,12 @@ export const TX_DESERIALIZATION_SCHEMA = { [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_CALL_CONTRACT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_CALL_CONTRACT_TX), - [OBJECT_TAG_PROOF_OF_INCLUSION]: TX_SCHEMA_FIELD(PROOF_OF_INCLUSION_TX, OBJECT_TAG_PROOF_OF_INCLUSION) + [OBJECT_TAG_PROOF_OF_INCLUSION]: TX_SCHEMA_FIELD(PROOF_OF_INCLUSION_TX, OBJECT_TAG_PROOF_OF_INCLUSION), + [OBJECT_TAG_STATE_TREES]: TX_SCHEMA_FIELD(STATE_TREES_TX, OBJECT_TAG_STATE_TREES), + [OBJECT_TAG_MERKLE_PATRICIA_TREE]: TX_SCHEMA_FIELD(MERKLE_PATRICIA_TREE_TX, OBJECT_TAG_MERKLE_PATRICIA_TREE), + [OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE]: TX_SCHEMA_FIELD(MERKLE_PATRICIA_TREE_VALUE_TX, OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE), + [OBJECT_TAG_CONTRACTS_TREE]: TX_SCHEMA_FIELD(CONTRACTS_TREE_TX, OBJECT_TAG_CONTRACTS_TREE), + [OBJECT_TAG_CONTRACT_CALLS_TREE]: TX_SCHEMA_FIELD(CONTRACT_CALLS_TREE_TX, OBJECT_TAG_CONTRACT_CALLS_TREE) } // VERIFICATION SCHEMA diff --git a/es/utils/mptree.js b/es/utils/mptree.js index 664454b52e..97de0f8b7e 100644 --- a/es/utils/mptree.js +++ b/es/utils/mptree.js @@ -15,28 +15,38 @@ * PERFORMANCE OF THIS SOFTWARE. */ +import { rlp, hash } from './crypto' + const NODE_TYPES = { branch: 1, extension: 2, leaf: 3 } -function nodeType (node, remainingPath) { +function nodeType (node) { if (node.length === 17) { return NODE_TYPES.branch } if (node.length === 2) { - const isOdd = remainingPath.length % 2 const nibble = node[0].toString('hex')[0] - if ((!isOdd && nibble === '0') || (isOdd && nibble === '1')) { + if (nibble === '0' || nibble === '1') { return NODE_TYPES.extension } - if ((!isOdd && nibble === '2') || (isOdd && nibble === '3')) { + if (nibble === '2' || nibble === '3') { return NODE_TYPES.leaf } } } +function decodePath (path) { + if (path[0] === '0' || path[0] === '2') { + return path.slice(2) + } + if (path[0] === '1' || path[0] === '3') { + return path.slice(1) + } +} + /** * Deserialize Merkle Patricia Tree * @rtype (binary: Array) => Object @@ -78,15 +88,59 @@ export function serialize (tree) { */ export function get (tree, key, hash) { const node = hash ? tree.nodes[hash] : tree.nodes[tree.rootHash] - const type = nodeType(node, key) + const type = nodeType(node) if (type === NODE_TYPES.branch) { - const nextHash = node[parseInt(key[0], 16)].toString('hex') - return get(tree, key.substr(1), nextHash) + if (key.length) { + const nextHash = node[parseInt(key[0], 16)].toString('hex') + return get(tree, key.substr(1), nextHash) + } + return node[16] + } + if (type === NODE_TYPES.extension) { + const path = decodePath(node[0].toString('hex')) + if (key.substr(0, path.length) === path) { + return get(tree, key.substr(path.length), node[1].toString('hex')) + } } - // TODO: handle NODE_TYPES.extension if (type === NODE_TYPES.leaf) { if (node[0].toString('hex').substr(1) === key) { return node[1] } } } + +function nodeHash (node) { + return Buffer.from(hash(rlp.encode(node))).toString('hex') +} + +/** + * Verify if rootHash of Merkle Patricia Tree is correct + * @rtype (tree: Object) => Boolean + * @param {Object} tree - Merkle Patricia Tree + * @return {Boolean} Boolean indicating whether or not rootHash is correct + */ +export function verify (tree, key, verified = []) { + const hash = key || tree.rootHash + if (verified.includes(hash)) { + return true + } + const node = tree.nodes[hash] + const type = nodeType(node) + if (nodeHash(node) !== hash) { + return false + } + verified.push(hash) + if (type === NODE_TYPES.branch) { + return !node.some((n, i) => { + const nextKey = n.toString('hex') + if (i < 16) { + return !verify(tree, nextKey, verified) + } + return false + }) + } + if (type === NODE_TYPES.extension) { + return verify(tree, node[1].toString('hex'), verified) + } + return true +} diff --git a/test/unit/mptree.js b/test/unit/mptree.js index c88c01402f..0c7879a40c 100644 --- a/test/unit/mptree.js +++ b/test/unit/mptree.js @@ -18,7 +18,7 @@ import '../' import { describe, it } from 'mocha' import { rlp } from '../../es/utils/crypto' -import { serialize, deserialize, get } from '../../es/utils/mptree' +import { serialize, deserialize, get, verify } from '../../es/utils/mptree' describe('Merkle Patricia Tree', function () { const binary = Buffer.from('f9013ea0d4b40fbf270d982d9c9bebc8acd6711db9a2465459f1cb67450f495e3a78f5d2f9011af850a0056232c6f764553f472dacd7bba764e4d630adce971e4437dcf07421e20d6cf3eea03e2e29b62366a6b1e363ebf174fce8e4d9ad61abdc2dde65e3f74923dcd629c48ccb0a010087038d7ea4c67ffcf850a065657db43209ef7d57acb7aaf2e2c38f8828f9d425e4bec0d7de5bfa26496c61eea03269a8e17fffe495df7b47bf0ffb94897e1060baf3192e99978d91010325b62d8ccb0a010087038d7ea4c68004f874a0d4b40fbf270d982d9c9bebc8acd6711db9a2465459f1cb67450f495e3a78f5d2f85180a065657db43209ef7d57acb7aaf2e2c38f8828f9d425e4bec0d7de5bfa26496c618080a0056232c6f764553f472dacd7bba764e4d630adce971e4437dcf07421e20d6cf3808080808080808080808080', 'hex') @@ -45,4 +45,11 @@ describe('Merkle Patricia Tree', function () { get(tree, key).toString('hex').should.equal(value) }) }) + + it('can verify root hash', () => { + const tree = deserialize(rlp.decode(binary)) + verify(tree).should.equal(true) + tree.nodes['65657db43209ef7d57acb7aaf2e2c38f8828f9d425e4bec0d7de5bfa26496c61'][1][3] = 13 + verify(tree).should.equal(false) + }) }) From dec757b38dbe26fe1345d647f5e2f2d9e66186fc Mon Sep 17 00:00:00 2001 From: nduchak Date: Sat, 30 Mar 2019 21:08:24 +0200 Subject: [PATCH 12/21] fix(schema.js): Fix linter error --- es/tx/builder/schema.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index d5b20d0400..f7cdfb92a4 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -176,7 +176,7 @@ export const OBJECT_ID_TX_TYPE = { [OBJECT_TAG_MERKLE_PATRICIA_TREE]: TX_TYPE.merklePatriciaTree, [OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE]: TX_TYPE.merklePatriciaTreeValue, [OBJECT_TAG_CONTRACTS_TREE]: TX_TYPE.contractsTree, - [OBJECT_TAG_CONTRACT_CALLS_TREE]: TX_TYPE.contractCallsTree, + [OBJECT_TAG_CONTRACT_CALLS_TREE]: TX_TYPE.contractCallsTree } export const FIELD_TYPES = { @@ -195,7 +195,7 @@ export const FIELD_TYPES = { offChainUpdates: 'offChainUpdates', callStack: 'callStack', proofOfInclusion: 'proofOfInclusion', - mptree: 'mptree', + mptree: 'mptree' } // FEE CALCULATION From cf9bcc5a4069793a1dc0a6925bbf16536646d5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Powaga?= Date: Mon, 1 Apr 2019 13:33:57 +0100 Subject: [PATCH 13/21] Improve channel tests and error handling (#276) * Make sure that sign function is correctly called * Improve error handling for update method --- test/integration/channel.js | 109 +++++++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/test/integration/channel.js b/test/integration/channel.js index 5e7da53ad4..181340723d 100644 --- a/test/integration/channel.js +++ b/test/integration/channel.js @@ -28,6 +28,12 @@ const wsUrl = process.env.WS_URL || 'ws://node:3014' plan('10000000000000000') +const identityContract = ` +contract Identity = + type state = () + public function main(x : int) = x +` + function waitForChannel (channel) { return new Promise(resolve => channel.on('statusChanged', (status) => { @@ -38,7 +44,7 @@ function waitForChannel (channel) { ) } -describe('Channel', function () { +describe.only('Channel', function () { configure(this) let initiator @@ -48,6 +54,9 @@ describe('Channel', function () { let responderShouldRejectUpdate let existingChannelId let offchainTx + let contractAddress + let contractEncodeCall + let callerNonce const initiatorSign = sinon.spy((tag, tx) => initiator.signTransaction(tx)) const responderSign = sinon.spy((tag, tx) => { if (responderShouldRejectUpdate) { @@ -73,7 +82,7 @@ describe('Channel', function () { responder.setKeypair(generateKeyPair()) sharedParams.initiatorId = await initiator.address() sharedParams.responderId = await responder.address() - await initiator.spend('2000000000000000', await responder.address()) + await initiator.spend('6000000000000000', await responder.address()) }) beforeEach(() => { @@ -378,6 +387,7 @@ describe('Channel', function () { initiatorCh = await Channel({ ...sharedParams, role: 'initiator', + port: 3002, existingChannelId, offchainTx, sign: initiatorSign @@ -385,6 +395,7 @@ describe('Channel', function () { responderCh = await Channel({ ...sharedParams, role: 'responder', + port: 3002, existingChannelId, offchainTx, sign: responderSign @@ -443,14 +454,14 @@ describe('Channel', function () { lockPeriod: 5, role: 'initiator', sign: initiatorSign, - port: 3002 + port: 3003 }) responderCh = await Channel({ ...sharedParams, lockPeriod: 5, role: 'responder', sign: responderSign, - port: 3002 + port: 3003 }) await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) const initiatorBalanceBeforeClose = await initiator.balance(initiatorAddr) @@ -459,7 +470,6 @@ describe('Channel', function () { const oldPoi = await initiatorCh.poi({ accounts: [initiatorAddr, responderAddr] }) - const oldBalances = await initiatorCh.balances([initiatorAddr, responderAddr]) const recentUpdate = await initiatorCh.update(initiatorAddr, responderAddr, 100, (tx) => initiator.signTransaction(tx)) const recentPoi = await responderCh.poi({ accounts: [initiatorAddr, responderAddr] @@ -499,18 +509,103 @@ describe('Channel', function () { ).should.be.equal(true) }) + it('can create a contract and accept', async () => { + initiatorCh = await Channel({ + ...sharedParams, + role: 'initiator', + port: 3004, + sign: initiatorSign + }) + responderCh = await Channel({ + ...sharedParams, + role: 'responder', + port: 3004, + sign: responderSign + }) + await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) + const code = await initiator.compileContractAPI(identityContract) + const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', []) + const result = await initiatorCh.createContract({ + code, + callData, + deposit: 1000, + vmVersion: 3, + abiVersion: 1 + }, async (tx) => await initiator.signTransaction(tx)) + result.should.eql({ accepted: true, address: result.address, state: initiatorCh.state() }) + contractAddress = result.address + contractEncodeCall = (method, args) => initiator.contractEncodeCallDataAPI(identityContract, method, args) + }) + + it('can create a contract and reject', async () => { + responderShouldRejectUpdate = true + const code = await initiator.compileContractAPI(identityContract) + const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', []) + const result = await initiatorCh.createContract({ + code, + callData, + deposit: 1000, + vmVersion: 3, + abiVersion: 1 + }, async (tx) => await initiator.signTransaction(tx)) + result.should.eql({ accepted: false }) + }) + + it('can call a contract and accept', async () => { + const result = await initiatorCh.callContract({ + amount: 0, + callData: await contractEncodeCall('main', ['42']), + contract: contractAddress, + abiVersion: 1 + }, async (tx) => await initiator.signTransaction(tx)) + result.should.eql({ accepted: true, state: initiatorCh.state() }) + callerNonce = Number(unpackTx(initiatorCh.state()).tx.encodedTx.tx.round) + }) + + it('can call a contract and reject', async () => { + responderShouldRejectUpdate = true + const result = await initiatorCh.callContract({ + amount: 0, + callData: await contractEncodeCall('main', ['42']), + contract: contractAddress, + abiVersion: 1 + }, async (tx) => await initiator.signTransaction(tx)) + result.should.eql({ accepted: false }) + }) + + it('can get contract call', async () => { + const result = await initiatorCh.getContractCall({ + caller: await initiator.address(), + contract: contractAddress, + round: callerNonce + }) + result.should.eql({ + callerId: await initiator.address(), + callerNonce, + contractId: contractAddress, + gasPrice: result.gasPrice, + gasUsed: result.gasUsed, + height: result.height, + log: result.log, + returnType: 'ok', + returnValue: result.returnValue + }) + const value = await initiator.contractDecodeDataAPI('int', result.returnValue) + value.should.eql({ type: 'word', value: 42 }) + }) + describe('throws errors', function () { before(async function () { initiatorCh = await Channel({ ...sharedParams, role: 'initiator', - port: 3003, + port: 3005, sign: initiatorSign }) responderCh = await Channel({ ...sharedParams, role: 'responder', - port: 3003, + port: 3005, sign: responderSign }) await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) From 1d2e3a68ce75af18f033b382e2ad547d75d262a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Powaga?= Date: Mon, 1 Apr 2019 14:15:23 +0100 Subject: [PATCH 14/21] Improve state channel params handling. Fixes #299 (#300) --- es/channel/internal.js | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/es/channel/internal.js b/es/channel/internal.js index 94bc2d9916..aaa710b755 100644 --- a/es/channel/internal.js +++ b/es/channel/internal.js @@ -35,7 +35,7 @@ const sequence = new WeakMap() const channelId = new WeakMap() const rpcCallbacks = new WeakMap() -function channelURL (url, { endpoint = 'channel', ...params }) { +function channelURL (url, params, endpoint = 'channel') { const paramString = R.join('&', R.values(R.mapObjIndexed((value, key) => `${pascalToSnake(key)}=${encodeURIComponent(value)}`, params))) @@ -169,28 +169,17 @@ function WebSocket (url, callbacks) { } async function initialize (channel, channelOptions) { - const params = R.pick([ - 'initiatorId', - 'responderId', - 'pushAmount', - 'initiatorAmount', - 'responderAmount', - 'channelReserve', - 'ttl', - 'host', - 'port', - 'lockPeriod', - 'role', - 'existingChannelId', - 'offchainTx' - ], channelOptions) + const optionsKeys = ['sign', 'endpoint', 'url'] + const params = R.pickBy(key => !optionsKeys.includes(key), channelOptions) + const { endpoint, url } = channelOptions + const wsUrl = channelURL(url, { ...params, protocol: 'json-rpc' }, endpoint) options.set(channel, channelOptions) fsm.set(channel, { handler: awaitingConnection }) eventEmitters.set(channel, new EventEmitter()) sequence.set(channel, 0) rpcCallbacks.set(channel, new Map()) - websockets.set(channel, await WebSocket(channelURL(channelOptions.url, { ...params, protocol: 'json-rpc' }), { + websockets.set(channel, await WebSocket(wsUrl, { onopen: () => changeStatus(channel, 'connected'), onclose: () => changeStatus(channel, 'disconnected'), onmessage: ({ data }) => onMessage(channel, data) From 8e01600098aa9ee438955178c5b59e94821e0412 Mon Sep 17 00:00:00 2001 From: naz_dou <41945483+nduchak@users.noreply.github.com> Date: Mon, 1 Apr 2019 16:42:36 +0300 Subject: [PATCH 15/21] Compiler improvements (#303) * refactor(Chain and Contract): Fix Chain.getAccount. Omprove Compiler Add ability to get account/balance on specific block hash/height. Add test. Add changeCompilerUrl to Compiler stamp #302 * fix(Crypto): Fix name hash function arguments parsing * refactor(Compiler): Remove async for changeCompilerUrl function --- docs/api/chain.md | 18 ++++++++++++++++++ docs/api/contract/aci.md | 6 +++--- es/chain/index.js | 14 ++++++++++++++ es/chain/node.js | 10 +++++++++- es/contract/compiler.js | 7 ++++++- es/utils/crypto.js | 6 +++--- es/utils/http.js | 2 +- test/integration/accounts.js | 14 ++++++++++++++ 8 files changed, 68 insertions(+), 9 deletions(-) diff --git a/docs/api/chain.md b/docs/api/chain.md index 15484aef8f..dc36c83995 100644 --- a/docs/api/chain.md +++ b/docs/api/chain.md @@ -26,6 +26,7 @@ import Chain from '@aeternity/aepp-sdk/es/chain' * *[.getMicroBlockTransactions()](#module_@aeternity/aepp-sdk/es/chain--Chain+getMicroBlockTransactions) ⇒ `Array.<Object>`* * *[.getKeyBlock()](#module_@aeternity/aepp-sdk/es/chain--Chain+getKeyBlock) ⇒ `Object`* * *[.getMicroBlockHeader()](#module_@aeternity/aepp-sdk/es/chain--Chain+getMicroBlockHeader) ⇒ `Object`* + * *[.getAccount(address, [options])](#module_@aeternity/aepp-sdk/es/chain--Chain+getAccount) ⇒ `Object`* * *[.txDryRun(txs, accounts, hashOrHeight)](#module_@aeternity/aepp-sdk/es/chain--Chain+txDryRun) ⇒ `Object`* * _static_ * [.waitMined(bool)](#module_@aeternity/aepp-sdk/es/chain--Chain.waitMined) ⇒ `Stamp` @@ -208,6 +209,23 @@ Get micro block header **Returns**: `Object` - Micro block header **Category**: async **rtype**: `(hash) => header: Object` + + +#### *chain.getAccount(address, [options]) ⇒ `Object`* +Get account by account public key + +**Kind**: instance abstract method of [`Chain`](#exp_module_@aeternity/aepp-sdk/es/chain--Chain) +**Returns**: `Object` - Account +**Category**: async +**rtype**: `(address, { hash, height }) => account: Object` + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| address | `String` | | Account public key | +| [options] | `Object` | {} | Options | +| [options.height] | `Number` | | Get account on specific block by block height | +| [options.hash] | `String` | | Get account on specific block by block hash | + #### *chain.txDryRun(txs, accounts, hashOrHeight) ⇒ `Object`* diff --git a/docs/api/contract/aci.md b/docs/api/contract/aci.md index a484701052..fcb15efbbd 100644 --- a/docs/api/contract/aci.md +++ b/docs/api/contract/aci.md @@ -37,7 +37,7 @@ Deploy contract | --- | --- | --- | --- | | init | `Array` | | Contract init function arguments array | | [options] | `Object` | {} | options Options object | -| [options.fromJsType] | `Boolean` | true | Validate and Transform arguments before prepare call-data | +| [options.skipArgsConvert] | `Boolean` | false | Skip Validation and Transforming arguments before prepare call-data | @@ -53,8 +53,8 @@ Call contract function | fn | `String` | | Function name | | params | `Array` | | Array of function arguments | | [options] | `Object` | {} | Array of function arguments | -| [options.fromJsType] | `Boolean` | true | Validate and Transform arguments before prepare call-data | -| [options.transformDecoded] | `Boolean` | true | Transform decoded data to JS type | +| [options.skipArgsConvert] | `Boolean` | false | Skip Validation and Transforming arguments before prepare call-data | +| [options.skipTransformDecoded] | `Boolean` | false | Skip Transform decoded data to JS type | diff --git a/es/chain/index.js b/es/chain/index.js index 153ddade71..8f62b895be 100644 --- a/es/chain/index.js +++ b/es/chain/index.js @@ -218,6 +218,20 @@ const Chain = Oracle.compose({ * @return {Object} Micro block header */ +/** + * Get account by account public key + * @function getAccount + * @instance + * @abstract + * @category async + * @rtype (address, { hash, height }) => account: Object + * @param {String} address - Account public key + * @param {Object} [options={}] - Options + * @param {Number} [options.height] - Get account on specific block by block height + * @param {String} [options.hash] - Get account on specific block by block hash + * @return {Object} Account + */ + /** * Transaction dry-run * @function txDryRun diff --git a/es/chain/node.js b/es/chain/node.js index 3fce61996d..c9f071bbf9 100644 --- a/es/chain/node.js +++ b/es/chain/node.js @@ -58,8 +58,15 @@ async function sendTransaction (tx, options = {}) { } } +async function getAccount (address, { height, hash } = {}) { + if (height) return this.api.getAccountByPubkeyAndHeight(address, height) + if (hash) return this.api.getAccountByPubkeyAndHash(address, hash) + return this.api.getAccountByPubkey(address) +} + async function balance (address, { height, hash, format = false } = {}) { - const { balance } = await this.api.getAccountByPubkey(address, { height, hash }) + const { balance } = await this.getAccount(address, { hash, height }) + return format ? formatBalance(balance) : balance.toString() } @@ -184,6 +191,7 @@ const ChainNode = Chain.compose(Node, Oracle, TransactionValidator, { methods: { sendTransaction, balance, + getAccount, topBlock, tx, height, diff --git a/es/contract/compiler.js b/es/contract/compiler.js index 71ac16891c..d731b2d190 100644 --- a/es/contract/compiler.js +++ b/es/contract/compiler.js @@ -48,6 +48,10 @@ async function contractGetACI (code, options = {}) { return this.http.post('/aci', { code, options }, options) } +function setCompilerUrl (url) { + this.http.changeBaseUrl(url) +} + /** * Contract Compiler Stamp * @@ -68,7 +72,8 @@ const ContractCompilerAPI = ContractBase.compose({ contractEncodeCallDataAPI, contractDecodeDataAPI, compileContractAPI, - contractGetACI + contractGetACI, + setCompilerUrl } }) diff --git a/es/utils/crypto.js b/es/utils/crypto.js index e7d7813a76..b678521048 100644 --- a/es/utils/crypto.js +++ b/es/utils/crypto.js @@ -84,10 +84,10 @@ export function addressFromDecimal (decimalAddress) { * Calculate 256bits Blake2b hash of `input` * @rtype (input: String) => hash: String * @param {String} input - Data to hash - * @return {String} Hash + * @return {Buffer} Hash */ export function hash (input) { - return blake2b(input, null, 32) // 256 bits + return Buffer.from(blake2b(input, null, 32)) // 256 bits } /** @@ -95,7 +95,7 @@ export function hash (input) { * as defined in https://github.com/aeternity/protocol/blob/master/AENS.md#hashing * @rtype (input: String) => hash: String * @param {String} input - Data to hash - * @return {String} Hash + * @return {Buffer} Hash */ export function nameId (input) { let buf = Buffer.allocUnsafe(32).fill(0) diff --git a/es/utils/http.js b/es/utils/http.js index 64751c828c..139793322e 100644 --- a/es/utils/http.js +++ b/es/utils/http.js @@ -6,7 +6,7 @@ import stampit from '@stamp/it' const axios = ax.create({ httpsAgent: new https.Agent({ - rejectUnauthorized: false + rejectUnauthorized: true // For develop }) }) diff --git a/test/integration/accounts.js b/test/integration/accounts.js index e353f383b8..c4cb79f3ca 100644 --- a/test/integration/accounts.js +++ b/test/integration/accounts.js @@ -75,6 +75,20 @@ describe('Accounts', function () { }) }) + it('Get Account by block height/hash', async () => { + const h = await wallet.height() + await wallet.awaitHeight(h + 3) + const spend = await wallet.spend(123, 'ak_DMNCzsVoZnpV5fe8FTQnNsTfQ48YM5C3WbHPsJyHjAuTXebFi') + await wallet.awaitHeight(spend.blockHeight + 2) + const accountAfterSpend = await wallet.getAccount(await wallet.address()) + const accountBeforeSpendByHash = await wallet.getAccount(await wallet.address(), { height: spend.blockHeight - 1 }) + BigNumber(accountBeforeSpendByHash.balance) + .minus(BigNumber(accountAfterSpend.balance)) + .toString() + .should.be + .equal(`${spend.tx.fee + spend.tx.amount}`) + }) + describe('can be configured to return th', () => { it('on creation', async () => { const wallet = await ready(this) From 988f0899d5ae17d9fa51a12b90f0df0ff93bafa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Powaga?= Date: Tue, 2 Apr 2019 14:47:40 +0100 Subject: [PATCH 16/21] Channel contracts (#279) * Add support for contracts in state channels * Remove console.log * Remove console.log * Improve channel rpc usage (#275) * Improve channel rpc usage * Fix lint error * Remove unreachable code * Improve channel tests and error handling (#276) * Make sure that sign function is correctly called * Improve error handling for update method * Improve state channel params handling. Fixes #299 (#300) --- es/channel/handlers.js | 53 +++++++++++++- es/channel/index.js | 138 +++++++++++++++++++++++++++++++++++- es/utils/crypto.js | 26 +++++++ test/integration/channel.js | 88 ++++++++++++++++++++++- 4 files changed, 301 insertions(+), 4 deletions(-) diff --git a/es/channel/handlers.js b/es/channel/handlers.js index 1bbac17b74..264a928fed 100644 --- a/es/channel/handlers.js +++ b/es/channel/handlers.js @@ -15,7 +15,7 @@ * PERFORMANCE OF THIS SOFTWARE. */ -import { generateKeyPair } from '../utils/crypto' +import { generateKeyPair, encodeContractAddress } from '../utils/crypto' import { options, changeStatus, @@ -24,6 +24,7 @@ import { emit, channelId } from './internal' +import { unpackTx } from '../tx/builder' export function awaitingConnection (channel, message, state) { if (message.method === 'channels.info') { @@ -310,6 +311,56 @@ export function awaitingDepositCompletion (channel, message, state) { } } +export async function awaitingNewContractTx (channel, message, state) { + if (message.method === 'channels.sign.update') { + const signedTx = await Promise.resolve(state.sign(message.params.data.tx)) + send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { tx: signedTx } }) + return { handler: awaitingNewContractCompletion, state } + } +} + +export function awaitingNewContractCompletion (channel, message, state) { + if (message.method === 'channels.update') { + const { round } = unpackTx(message.params.data.state).tx.encodedTx.tx + // eslint-disable-next-line standard/computed-property-even-spacing + const owner = options.get(channel)[{ + initiator: 'initiatorId', + responder: 'responderId' + }[options.get(channel).role]] + changeState(channel, message.params.data.state) + state.resolve({ + accepted: true, + address: encodeContractAddress(owner, round), + state: message.params.data.state + }) + return { handler: channelOpen } + } + if (message.method === 'channels.conflict') { + state.resolve({ accepted: false }) + return { handler: channelOpen } + } +} + +export async function awaitingCallContractUpdateTx (channel, message, state) { + if (message.method === 'channels.sign.update') { + const signedTx = await Promise.resolve(state.sign(message.params.data.tx)) + send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { tx: signedTx } }) + return { handler: awaitingCallContractCompletion, state } + } +} + +export function awaitingCallContractCompletion (channel, message, state) { + if (message.method === 'channels.update') { + changeState(channel, message.params.data.state) + state.resolve({ accepted: true, state: message.params.data.state }) + return { handler: channelOpen } + } + if (message.method === 'channels.conflict') { + state.resolve({ accepted: false }) + return { handler: channelOpen } + } +} + export function channelClosed (channel, message, state) { return { handler: channelClosed } } diff --git a/es/channel/index.js b/es/channel/index.js index ea2c31cd22..200870a494 100644 --- a/es/channel/index.js +++ b/es/channel/index.js @@ -23,6 +23,7 @@ */ import AsyncInit from '../utils/async-init' +import { snakeToPascal } from '../utils/string' import * as handlers from './handlers' import { eventEmitters, @@ -192,7 +193,7 @@ function shutdown (sign) { return new Promise((resolve) => { enqueueAction( this, - (channel, state) => true, + (channel, state) => state.handler === handlers.channelOpen, (channel, state) => { send(channel, { jsonrpc: '2.0', method: 'channels.shutdown', params: {} }) return { @@ -297,6 +298,136 @@ function deposit (amount, sign, { onOnChainTx, onOwnDepositLocked, onDepositLock }) } +/** + * Create a contract + * + * @param {object} options + * @param {string} [options.code] - Api encoded compiled AEVM byte code + * @param {string} [options.callData] - Api encoded compiled AEVM call data for the code + * @param {number} [options.deposit] - Initial amount the owner of the contract commits to it + * @param {number} [options.vmVersion] - Version of the AEVM + * @param {number} [options.abiVersion] - Version of the ABI + * @param {function} sign - Function which verifies and signs create contract transaction + * @return {Promise} + * @example channel.createContract({ + * code: 'cb_HKtpipK4aCgYb17wZ...', + * callData: 'cb_1111111111111111...', + * deposit: 10, + * vmVersion: 3, + * abiVersion: 1 + * }).then(({ accepted, state, address }) => { + * if (accepted) { + * console.log('New contract has been created') + * console.log('Contract address:', address) + * } else { + * console.log('New contract has been rejected') + * } + * }) + */ +function createContract ({ code, callData, deposit, vmVersion, abiVersion }, sign) { + return new Promise((resolve) => { + enqueueAction( + this, + (channel, state) => state.handler === handlers.channelOpen, + (channel, state) => { + send(channel, { + jsonrpc: '2.0', + method: 'channels.update.new_contract', + params: { + code, + call_data: callData, + deposit, + vm_version: vmVersion, + abi_version: abiVersion + } + }) + return { + handler: handlers.awaitingNewContractTx, + state: { + sign, + resolve + } + } + } + ) + }) +} + +/** + * Call a contract + * + * @param {object} options + * @param {string} [options.amount] - Amount the caller of the contract commits to it + * @param {string} [options.callData] - ABI encoded compiled AEVM call data for the code + * @param {number} [options.contract] - Address of the contract to call + * @param {number} [options.abiVersion] - Version of the ABI + * @param {function} sign - Function which verifies and signs contract call transaction + * @return {Promise} + * @example channel.callContract({ + * contract: 'ct_9sRA9AVE4BYTAkh5RNfJYmwQe1NZ4MErasQLXZkFWG43TPBqa', + * callData: 'cb_1111111111111111...', + * amount: 0, + * abiVersion: 1 + * }).then(({ accepted, state }) => { + * if (accepted) { + * console.log('Contract called succesfully') + * console.log('The new state is:', state) + * } else { + * console.log('Contract call has been rejected') + * } + * }) + */ +function callContract ({ amount, callData, contract, abiVersion }, sign) { + return new Promise((resolve) => { + enqueueAction( + this, + (channel, state) => state.handler === handlers.channelOpen, + (channel, state) => { + send(channel, { + jsonrpc: '2.0', + method: 'channels.update.call_contract', + params: { + amount, + call_data: callData, + contract, + abi_version: abiVersion + } + }) + return { + handler: handlers.awaitingCallContractUpdateTx, + state: { resolve, sign } + } + } + ) + }) +} + +/** + * Get contract call result + * + * @param {object} options + * @param {string} [options.caller] - Address of contract caller + * @param {string} [options.contract] - Address of the contract + * @param {number} [options.round] - Round when contract was called + * @return {Promise} + * @example channel.getContractCall({ + * caller: 'ak_Y1NRjHuoc3CGMYMvCmdHSBpJsMDR6Ra2t5zjhRcbtMeXXLpLH', + * contract: 'ct_9sRA9AVE4BYTAkh5RNfJYmwQe1NZ4MErasQLXZkFWG43TPBqa', + * round: 3 + * }).then(({ returnType, returnValue }) => { + * if (returnType === 'ok') console.log(returnValue) + * }) + */ +async function getContractCall ({ caller, contract, round }) { + const result = await call(this, 'channels.get.contract_call', { caller, contract, round }) + return R.fromPairs( + R.map( + ([key, value]) => ([snakeToPascal(key), value]), + R.toPairs(result) + ) + ) +} + /** * Send generic message * @@ -382,7 +513,10 @@ const Channel = AsyncInit.compose({ shutdown, sendMessage, withdraw, - deposit + deposit, + createContract, + callContract, + getContractCall } }) diff --git a/es/utils/crypto.js b/es/utils/crypto.js index b678521048..e4951e357d 100644 --- a/es/utils/crypto.js +++ b/es/utils/crypto.js @@ -208,6 +208,32 @@ export function hexStringToByte (str) { return new Uint8Array(a) } +/** + * Converts a positive integer to the smallest possible + * representation in a binary digit representation + * @rtype (value: Number) => Buffer + * @param {Number} value - Value to encode + * @return {Buffer} - Encoded data + */ +export function encodeUnsigned (value) { + const binary = Buffer.allocUnsafe(4) + binary.writeUInt32BE(value) + return binary.slice(binary.findIndex(i => i !== 0)) +} + +/** + * Compute contract address + * @rtype (owner: String, nonce: Number) => String + * @param {String} owner - Address of contract owner + * @param {Number} nonce - Round when contract was created + * @return {String} - Contract address + */ +export function encodeContractAddress (owner, nonce) { + const publicKey = decodeBase58Check(assertedType(owner, 'ak')) + const binary = Buffer.concat([publicKey, encodeUnsigned(nonce)]) + return `ct_${encodeBase58Check(hash(binary))}` +} + // KEY-PAIR HELPERS /** diff --git a/test/integration/channel.js b/test/integration/channel.js index 181340723d..a386ce073f 100644 --- a/test/integration/channel.js +++ b/test/integration/channel.js @@ -44,7 +44,7 @@ function waitForChannel (channel) { ) } -describe.only('Channel', function () { +describe('Channel', function () { configure(this) let initiator @@ -403,6 +403,92 @@ describe.only('Channel', function () { await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) sinon.assert.notCalled(initiatorSign) sinon.assert.notCalled(responderSign) + await initiatorCh.leave() + }) + + it('can create a contract and accept', async () => { + initiatorCh = await Channel({ + ...sharedParams, + role: 'initiator', + port: 3003, + sign: initiatorSign + }) + responderCh = await Channel({ + ...sharedParams, + role: 'responder', + port: 3003, + sign: responderSign + }) + await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) + const code = await initiator.compileContractAPI(identityContract) + const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', []) + const result = await initiatorCh.createContract({ + code, + callData, + deposit: 1000, + vmVersion: 3, + abiVersion: 1 + }, async (tx) => await initiator.signTransaction(tx)) + result.should.eql({ accepted: true, address: result.address, state: initiatorCh.state() }) + contractAddress = result.address + contractEncodeCall = (method, args) => initiator.contractEncodeCallDataAPI(identityContract, method, args) + }) + + it('can create a contract and reject', async () => { + responderShouldRejectUpdate = true + const code = await initiator.compileContractAPI(identityContract) + const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', []) + const result = await initiatorCh.createContract({ + code, + callData, + deposit: 1000, + vmVersion: 3, + abiVersion: 1 + }, async (tx) => await initiator.signTransaction(tx)) + result.should.eql({ accepted: false }) + }) + + it('can call a contract and accept', async () => { + const result = await initiatorCh.callContract({ + amount: 0, + callData: await contractEncodeCall('main', ['42']), + contract: contractAddress, + abiVersion: 1 + }, async (tx) => await initiator.signTransaction(tx)) + result.should.eql({ accepted: true, state: initiatorCh.state() }) + callerNonce = Number(unpackTx(initiatorCh.state()).tx.encodedTx.tx.round) + }) + + it('can call a contract and reject', async () => { + responderShouldRejectUpdate = true + const result = await initiatorCh.callContract({ + amount: 0, + callData: await contractEncodeCall('main', ['42']), + contract: contractAddress, + abiVersion: 1 + }, async (tx) => await initiator.signTransaction(tx)) + result.should.eql({ accepted: false }) + }) + + it('can get contract call', async () => { + const result = await initiatorCh.getContractCall({ + caller: await initiator.address(), + contract: contractAddress, + round: callerNonce + }) + result.should.eql({ + callerId: await initiator.address(), + callerNonce, + contractId: contractAddress, + gasPrice: result.gasPrice, + gasUsed: result.gasUsed, + height: result.height, + log: result.log, + returnType: 'ok', + returnValue: result.returnValue + }) + const value = await initiator.contractDecodeDataAPI('int', result.returnValue) + value.should.eql({ type: 'word', value: 42 }) }) it('can solo close a channel', async () => { From 01b004ac6c76d758329ee5aa5a60265e76b920f5 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Wed, 10 Apr 2019 14:48:04 +0100 Subject: [PATCH 17/21] Fix channel tests --- test/integration/channel.js | 144 +++++++++--------------------------- 1 file changed, 37 insertions(+), 107 deletions(-) diff --git a/test/integration/channel.js b/test/integration/channel.js index 432dc4fe9f..d68fe44ebe 100644 --- a/test/integration/channel.js +++ b/test/integration/channel.js @@ -406,7 +406,7 @@ describe('Channel', function () { await initiatorCh.leave() }) - it('can create a contract and accept', async () => { + it('can solo close a channel', async () => { initiatorCh = await Channel({ ...sharedParams, role: 'initiator', @@ -420,109 +420,17 @@ describe('Channel', function () { sign: responderSign }) await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) - const code = await initiator.compileContractAPI(identityContract) - const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', []) - const result = await initiatorCh.createContract({ - code, - callData, - deposit: 1000, - vmVersion: 3, - abiVersion: 1 - }, async (tx) => await initiator.signTransaction(tx)) - result.should.eql({ accepted: true, address: result.address, signedTx: (await initiatorCh.state()).signedTx }) - contractAddress = result.address - contractEncodeCall = (method, args) => initiator.contractEncodeCallDataAPI(identityContract, method, args) - }) - - it('can create a contract and reject', async () => { - responderShouldRejectUpdate = true - const code = await initiator.compileContractAPI(identityContract) - const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', []) - const result = await initiatorCh.createContract({ - code, - callData, - deposit: 1000, - vmVersion: 3, - abiVersion: 1 - }, async (tx) => await initiator.signTransaction(tx)) - result.should.eql({ accepted: false }) - }) - - it('can call a contract and accept', async () => { - const result = await initiatorCh.callContract({ - amount: 0, - callData: await contractEncodeCall('main', ['42']), - contract: contractAddress, - abiVersion: 1 - }, async (tx) => await initiator.signTransaction(tx)) - result.should.eql({ accepted: true, signedTx: (await initiatorCh.state()).signedTx }) - callerNonce = Number(unpackTx((await initiatorCh.state()).signedTx).tx.encodedTx.tx.round) - }) - - it('can call a contract and reject', async () => { - responderShouldRejectUpdate = true - const result = await initiatorCh.callContract({ - amount: 0, - callData: await contractEncodeCall('main', ['42']), - contract: contractAddress, - abiVersion: 1 - }, async (tx) => await initiator.signTransaction(tx)) - result.should.eql({ accepted: false }) - }) - - it('can call a contract using dry-run', async () => { - const result = await initiatorCh.callContractStatic({ - amount: 0, - callData: await contractEncodeCall('main', ['42']), - contract: contractAddress, - abiVersion: 1 - }) - result.should.eql({ - callerId: await initiator.address(), - callerNonce: result.callerNonce, - contractId: contractAddress, - gasPrice: result.gasPrice, - gasUsed: result.gasUsed, - height: result.height, - log: result.log, - returnType: 'ok', - returnValue: result.returnValue - }) - const value = await initiator.contractDecodeDataAPI('int', result.returnValue) - value.should.eql({ type: 'word', value: 42 }) - }) - it('can get contract call', async () => { - const result = await initiatorCh.getContractCall({ - caller: await initiator.address(), - contract: contractAddress, - round: callerNonce - }) - result.should.eql({ - callerId: await initiator.address(), - callerNonce, - contractId: contractAddress, - gasPrice: result.gasPrice, - gasUsed: result.gasUsed, - height: result.height, - log: result.log, - returnType: 'ok', - returnValue: result.returnValue - }) - const value = await initiator.contractDecodeDataAPI('int', result.returnValue) - value.should.eql({ type: 'word', value: 42 }) - }) - it('can solo close a channel', async () => { const initiatorAddr = await initiator.address() const responderAddr = await responder.address() - const { state } = await initiatorCh.update( + const { signedTx } = await initiatorCh.update( await initiator.address(), await responder.address(), 100, tx => initiator.signTransaction(tx) ) const poi = await initiatorCh.poi({ - accounts: [initiatorAddr, responderAddr] + accounts: [initiatorAddr, responderAddr], }) const balances = await initiatorCh.balances([initiatorAddr, responderAddr]) const initiatorBalanceBeforeClose = await initiator.balance(initiatorAddr) @@ -531,7 +439,7 @@ describe('Channel', function () { channelId: await initiatorCh.id(), fromId: initiatorAddr, poi, - payload: state + payload: signedTx }) const closeSoloTxFee = unpackTx(closeSoloTx).tx.fee await initiator.sendTransaction(await initiator.signTransaction(closeSoloTx), { waitMined: true }) @@ -561,14 +469,14 @@ describe('Channel', function () { lockPeriod: 5, role: 'initiator', sign: initiatorSign, - port: 3003 + port: 3004 }) responderCh = await Channel({ ...sharedParams, lockPeriod: 5, role: 'responder', sign: responderSign, - port: 3003 + port: 3004 }) await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) const initiatorBalanceBeforeClose = await initiator.balance(initiatorAddr) @@ -586,7 +494,7 @@ describe('Channel', function () { channelId: initiatorCh.id(), fromId: initiatorAddr, poi: oldPoi, - payload: oldUpdate.state + payload: oldUpdate.signedTx }) const closeSoloTxFee = unpackTx(closeSoloTx).tx.fee await initiator.sendTransaction(await initiator.signTransaction(closeSoloTx), { waitMined: true }) @@ -594,7 +502,7 @@ describe('Channel', function () { channelId: responderCh.id(), fromId: responderAddr, poi: recentPoi, - payload: recentUpdate.state + payload: recentUpdate.signedTx }) const slashTxFee = unpackTx(slashTx).tx.fee await responder.sendTransaction(await responder.signTransaction(slashTx), { waitMined: true }) @@ -620,13 +528,13 @@ describe('Channel', function () { initiatorCh = await Channel({ ...sharedParams, role: 'initiator', - port: 3004, + port: 3005, sign: initiatorSign }) responderCh = await Channel({ ...sharedParams, role: 'responder', - port: 3004, + port: 3005, sign: responderSign }) await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) @@ -639,7 +547,7 @@ describe('Channel', function () { vmVersion: 3, abiVersion: 1 }, async (tx) => await initiator.signTransaction(tx)) - result.should.eql({ accepted: true, address: result.address, state: initiatorCh.state() }) + result.should.eql({ accepted: true, address: result.address, signedTx: (await initiatorCh.state()).signedTx }) contractAddress = result.address contractEncodeCall = (method, args) => initiator.contractEncodeCallDataAPI(identityContract, method, args) }) @@ -665,8 +573,8 @@ describe('Channel', function () { contract: contractAddress, abiVersion: 1 }, async (tx) => await initiator.signTransaction(tx)) - result.should.eql({ accepted: true, state: initiatorCh.state() }) - callerNonce = Number(unpackTx(initiatorCh.state()).tx.encodedTx.tx.round) + result.should.eql({ accepted: true, signedTx: (await initiatorCh.state()).signedTx }) + callerNonce = Number(unpackTx((await initiatorCh.state()).signedTx).tx.encodedTx.tx.round) }) it('can call a contract and reject', async () => { @@ -700,6 +608,28 @@ describe('Channel', function () { const value = await initiator.contractDecodeDataAPI('int', result.returnValue) value.should.eql({ type: 'word', value: 42 }) }) + + it('can call a contract using dry-run', async () => { + const result = await initiatorCh.callContractStatic({ + amount: 0, + callData: await contractEncodeCall('main', ['42']), + contract: contractAddress, + abiVersion: 1 + }) + result.should.eql({ + callerId: await initiator.address(), + callerNonce: result.callerNonce, + contractId: contractAddress, + gasPrice: result.gasPrice, + gasUsed: result.gasUsed, + height: result.height, + log: result.log, + returnType: 'ok', + returnValue: result.returnValue + }) + const value = await initiator.contractDecodeDataAPI('int', result.returnValue) + value.should.eql({ type: 'word', value: 42 }) + }) it('can get contract state', async () => { const result = await initiatorCh.getContractState(contractAddress) @@ -723,13 +653,13 @@ describe('Channel', function () { initiatorCh = await Channel({ ...sharedParams, role: 'initiator', - port: 3005, + port: 3006, sign: initiatorSign }) responderCh = await Channel({ ...sharedParams, role: 'responder', - port: 3005, + port: 3006, sign: responderSign }) await Promise.all([waitForChannel(initiatorCh), waitForChannel(responderCh)]) From a97e30ffd3390f2d23a34af165f0cea12002899e Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Fri, 12 Apr 2019 12:44:05 +0100 Subject: [PATCH 18/21] Add contract call tx serialization --- es/tx/builder/index.js | 12 ++++++++++++ es/tx/builder/schema.js | 22 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/es/tx/builder/index.js b/es/tx/builder/index.js index 28b566d903..1214ab1539 100644 --- a/es/tx/builder/index.js +++ b/es/tx/builder/index.js @@ -63,6 +63,12 @@ function deserializeField (value, type, prefix) { return [readInt(value)] case FIELD_TYPES.mptree: return value.map(mpt.deserialize) + case FIELD_TYPES.callReturnType: + switch (readInt(value)) { + case '0': return 'ok' + case '1': return 'error' + case '2': return 'revert' + } default: return value } @@ -88,6 +94,12 @@ function serializeField (value, type, prefix) { return buildPointers(value) case FIELD_TYPES.mptree: return value.map(mpt.serialize) + case FIELD_TYPES.callReturnType: + switch (value) { + case 'ok': return writeInt(0) + case 'error': return writeInt(1) + case 'revert': return writeInt(2) + } default: return value } diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index f7cdfb92a4..667dd7dc53 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -37,6 +37,7 @@ const OBJECT_TAG_NAME_SERVICE_UPDATE_TRANSACTION = 34 const OBJECT_TAG_NAME_SERVICE_REVOKE_TRANSACTION = 35 const OBJECT_TAG_NAME_SERVICE_TRANSFER_TRANSACTION = 36 const OBJECT_TAG_CONTRACT = 40 +const OBJECT_TAG_CONTRACT_CALL = 41 const OBJECT_TAG_CONTRACT_CREATE_TRANSACTION = 42 const OBJECT_TAG_CONTRACT_CALL_TRANSACTION = 43 const OBJECT_TAG_CHANNEL_CREATE_TX = 50 @@ -111,6 +112,7 @@ export const TX_TYPE = { contract: 'contract', contractCreate: 'contractCreateTx', contractCall: 'contractCallTx', + contractCallResult: 'contractCallResult', // ORACLE oracleRegister: 'oracleRegister', oracleExtend: 'oracleExtend', @@ -152,6 +154,7 @@ export const OBJECT_ID_TX_TYPE = { [OBJECT_TAG_CONTRACT]: TX_TYPE.contract, [OBJECT_TAG_CONTRACT_CREATE_TRANSACTION]: TX_TYPE.contractCreate, [OBJECT_TAG_CONTRACT_CALL_TRANSACTION]: TX_TYPE.contractCall, + [OBJECT_TAG_CONTRACT_CALL]: TX_TYPE.contractCallResult, // ORACLE [OBJECT_TAG_ORACLE_REGISTER_TRANSACTION]: TX_TYPE.oracleRegister, [OBJECT_TAG_ORACLE_EXTEND_TRANSACTION]: TX_TYPE.oracleExtend, @@ -195,7 +198,8 @@ export const FIELD_TYPES = { offChainUpdates: 'offChainUpdates', callStack: 'callStack', proofOfInclusion: 'proofOfInclusion', - mptree: 'mptree' + mptree: 'mptree', + callReturnType: 'callReturnType' } // FEE CALCULATION @@ -378,6 +382,20 @@ const CONTRACT_CALL_TX = [ TX_FIELD('callData', FIELD_TYPES.binary, 'cb') ] +const CONTRACT_CALL_RESULT_TX = [ + ...BASE_TX, + TX_FIELD('callerId', FIELD_TYPES.id, 'ak'), + TX_FIELD('callerNonce', FIELD_TYPES.int), + TX_FIELD('height', FIELD_TYPES.int), + TX_FIELD('contractId', FIELD_TYPES.id, 'ct'), + TX_FIELD('gasPrice', FIELD_TYPES.int), + TX_FIELD('gasUsed', FIELD_TYPES.int), + TX_FIELD('returnValue', FIELD_TYPES.binary, 'cb'), + TX_FIELD('returnType', FIELD_TYPES.callReturnType), + // TODO: add serialization for :: [ {
:: id, [ :: binary() ], :: binary() } ] + TX_FIELD('log', FIELD_TYPES.rawBinary) +] + const ORACLE_REGISTER_TX = [ ...BASE_TX, TX_FIELD('accountId', FIELD_TYPES.id, 'ak'), @@ -612,6 +630,7 @@ export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.contract]: TX_SCHEMA_FIELD(CONTRACT_TX, OBJECT_TAG_CONTRACT), [TX_TYPE.contractCreate]: TX_SCHEMA_FIELD(CONTRACT_CREATE_TX, OBJECT_TAG_CONTRACT_CREATE_TRANSACTION), [TX_TYPE.contractCall]: TX_SCHEMA_FIELD(CONTRACT_CALL_TX, OBJECT_TAG_CONTRACT_CALL_TRANSACTION), + [TX_TYPE.contractCallResult]: TX_SCHEMA_FIELD(CONTRACT_CALL_RESULT_TX, OBJECT_TAG_CONTRACT_CALL), [TX_TYPE.oracleRegister]: TX_SCHEMA_FIELD(ORACLE_REGISTER_TX, OBJECT_TAG_ORACLE_REGISTER_TRANSACTION), [TX_TYPE.oracleExtend]: TX_SCHEMA_FIELD(ORACLE_EXTEND_TX, OBJECT_TAG_ORACLE_EXTEND_TRANSACTION), [TX_TYPE.oracleQuery]: TX_SCHEMA_FIELD(ORACLE_QUERY_TX, OBJECT_TAG_ORACLE_QUERY_TRANSACTION), @@ -649,6 +668,7 @@ export const TX_DESERIALIZATION_SCHEMA = { [OBJECT_TAG_CONTRACT]: TX_SCHEMA_FIELD(CONTRACT_TX, OBJECT_TAG_CONTRACT), [OBJECT_TAG_CONTRACT_CREATE_TRANSACTION]: TX_SCHEMA_FIELD(CONTRACT_CREATE_TX, OBJECT_TAG_CONTRACT_CREATE_TRANSACTION), [OBJECT_TAG_CONTRACT_CALL_TRANSACTION]: TX_SCHEMA_FIELD(CONTRACT_CALL_TX, OBJECT_TAG_CONTRACT_CALL_TRANSACTION), + [OBJECT_TAG_CONTRACT_CALL]: TX_SCHEMA_FIELD(CONTRACT_CALL_RESULT_TX, OBJECT_TAG_CONTRACT_CALL), [OBJECT_TAG_ORACLE_REGISTER_TRANSACTION]: TX_SCHEMA_FIELD(ORACLE_REGISTER_TX, OBJECT_TAG_ORACLE_REGISTER_TRANSACTION), [OBJECT_TAG_ORACLE_EXTEND_TRANSACTION]: TX_SCHEMA_FIELD(ORACLE_EXTEND_TX, OBJECT_TAG_ORACLE_EXTEND_TRANSACTION), [OBJECT_TAG_ORACLE_QUERY_TRANSACTION]: TX_SCHEMA_FIELD(ORACLE_QUERY_TX, OBJECT_TAG_ORACLE_QUERY_TRANSACTION), From 6822692f848990a2fc11938b765c0aa633c16236 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Fri, 12 Apr 2019 18:10:13 +0100 Subject: [PATCH 19/21] Add channel tx serialization --- es/tx/builder/index.js | 4 ++++ es/tx/builder/schema.js | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/es/tx/builder/index.js b/es/tx/builder/index.js index 1214ab1539..d4bf9826cd 100644 --- a/es/tx/builder/index.js +++ b/es/tx/builder/index.js @@ -68,6 +68,7 @@ function deserializeField (value, type, prefix) { case '0': return 'ok' case '1': return 'error' case '2': return 'revert' + default: return value } default: return value @@ -86,6 +87,8 @@ function serializeField (value, type, prefix) { return Buffer.from([value ? 1 : 0]) case FIELD_TYPES.binary: return decode(value, prefix) + case FIELD_TYPES.hex: + return Buffer.from(value, 'hex') case FIELD_TYPES.signatures: return value.map(Buffer.from) case FIELD_TYPES.string: @@ -99,6 +102,7 @@ function serializeField (value, type, prefix) { case 'ok': return writeInt(0) case 'error': return writeInt(1) case 'revert': return writeInt(2) + default: return value } default: return value diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index 667dd7dc53..f6e4abc421 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -48,6 +48,7 @@ const OBJECT_TAG_CHANNEL_CLOSE_SOLO_TX = 54 const OBJECT_TAG_CHANNEL_SLASH_TX = 55 const OBJECT_TAG_CHANNEL_SETTLE_TX = 56 const OBJECT_TAG_CHANNEL_OFFCHAIN_TX = 57 +const OBJECT_TAG_CHANNEL = 58 const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX = 570 const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX = 571 const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX = 572 @@ -127,6 +128,7 @@ export const TX_TYPE = { channelWithdraw: 'channelWithdraw', channelSettle: 'channelSettle', channelOffChain: 'channelOffChain', + channel: 'channel', channelOffChainUpdateTransfer: 'channelOffChainUpdateTransfer', channelOffChainUpdateDeposit: 'channelOffChainUpdateDeposit', channelOffChainUpdateWithdrawal: 'channelOffChainUpdateWithdrawal', @@ -169,6 +171,7 @@ export const OBJECT_ID_TX_TYPE = { [OBJECT_TAG_CHANNEL_WITHRAW_TX]: TX_TYPE.channelWithdraw, [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_TYPE.channelSettle, [OBJECT_TAG_CHANNEL_OFFCHAIN_TX]: TX_TYPE.channelOffChain, + [OBJECT_TAG_CHANNEL]: TX_TYPE.channel, [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX]: TX_TYPE.channelOffChainUpdateTransfer, [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX]: TX_TYPE.channelOffChainUpdateDeposit, [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX]: TX_TYPE.channelOffChainUpdateWithdrawal, @@ -537,6 +540,22 @@ const CHANNEL_OFFCHAIN_TX = [ TX_FIELD('stateHash', FIELD_TYPES.binary, 'st') ] +const CHANNEL_TX = [ + ...BASE_TX, + TX_FIELD('initiator', FIELD_TYPES.id, 'ak'), + TX_FIELD('responder', FIELD_TYPES.id, 'ak'), + TX_FIELD('channelAmount', FIELD_TYPES.int), + TX_FIELD('initiatorAmount', FIELD_TYPES.int), + TX_FIELD('responderAmount', FIELD_TYPES.int), + TX_FIELD('channelReserve', FIELD_TYPES.int), + TX_FIELD('delegateIds', FIELD_TYPES.ids), + TX_FIELD('stateHash', FIELD_TYPES.hex), + TX_FIELD('round', FIELD_TYPES.int), + TX_FIELD('soloRound', FIELD_TYPES.int), + TX_FIELD('lockPeriod', FIELD_TYPES.int), + TX_FIELD('lockedUntil', FIELD_TYPES.int) +] + const CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX = [ ...BASE_TX, TX_FIELD('owner', FIELD_TYPES.id, 'ak'), @@ -643,6 +662,7 @@ export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.channelWithdraw]: TX_SCHEMA_FIELD(CHANNEL_WITHDRAW_TX, OBJECT_TAG_CHANNEL_WITHRAW_TX), [TX_TYPE.channelSettle]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX), [TX_TYPE.channelOffChain]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_TX), + [TX_TYPE.channel]: TX_SCHEMA_FIELD(CHANNEL_TX, OBJECT_TAG_CHANNEL), [TX_TYPE.channelOffChainUpdateTransfer]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX), [TX_TYPE.channelOffChainUpdateDeposit]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX), [TX_TYPE.channelOffChainUpdateWithdrawal]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX), @@ -681,6 +701,7 @@ export const TX_DESERIALIZATION_SCHEMA = { [OBJECT_TAG_CHANNEL_WITHRAW_TX]: TX_SCHEMA_FIELD(CHANNEL_WITHDRAW_TX, OBJECT_TAG_CHANNEL_WITHRAW_TX), [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_TX), + [OBJECT_TAG_CHANNEL]: TX_SCHEMA_FIELD(CHANNEL_TX, OBJECT_TAG_CHANNEL), [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX), From fb70fb1baabef90aca1fde748ad403ba28a9de60 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Fri, 12 Apr 2019 18:24:00 +0100 Subject: [PATCH 20/21] Add missing tree tx serializations --- es/tx/builder/schema.js | 48 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index f6e4abc421..a612449360 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -60,6 +60,10 @@ const OBJECT_TAG_MERKLE_PATRICIA_TREE = 63 const OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE = 64 const OBJECT_TAG_CONTRACTS_TREE = 621 const OBJECT_TAG_CONTRACT_CALLS_TREE = 622 +const OBJECT_TAG_CHANNELS_TREE = 623 +const OBJECT_TAG_NAMESERVICE_TREE = 624 +const OBJECT_TAG_ORACLES_TREE = 625 +const OBJECT_TAG_ACCOUNTS_TREE = 626 const TX_FIELD = (name, type, prefix) => [name, type, prefix] const TX_SCHEMA_FIELD = (schema, objectId) => [schema, objectId] @@ -139,7 +143,11 @@ export const TX_TYPE = { merklePatriciaTree: 'merklePatriciaTree', merklePatriciaTreeValue: 'merklePatriciaTreeValue', contractsTree: 'contractsTree', - contractCallsTree: 'contractCallsTree' + contractCallsTree: 'contractCallsTree', + channelsTree: 'channelsTree', + nameserviceTree: 'nameserviceTree', + oraclesTree: 'oraclesTree', + accountsTree: 'accountsTree' } export const OBJECT_ID_TX_TYPE = { @@ -182,7 +190,11 @@ export const OBJECT_ID_TX_TYPE = { [OBJECT_TAG_MERKLE_PATRICIA_TREE]: TX_TYPE.merklePatriciaTree, [OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE]: TX_TYPE.merklePatriciaTreeValue, [OBJECT_TAG_CONTRACTS_TREE]: TX_TYPE.contractsTree, - [OBJECT_TAG_CONTRACT_CALLS_TREE]: TX_TYPE.contractCallsTree + [OBJECT_TAG_CONTRACT_CALLS_TREE]: TX_TYPE.contractCallsTree, + [OBJECT_TAG_CHANNELS_TREE]: TX_TYPE.channelsTree, + [OBJECT_TAG_NAMESERVICE_TREE]: TX_TYPE.nameserviceTree, + [OBJECT_TAG_ORACLES_TREE]: TX_TYPE.oraclesTree, + [OBJECT_TAG_ACCOUNTS_TREE]: TX_TYPE.accountsTree } export const FIELD_TYPES = { @@ -637,6 +649,26 @@ const CONTRACT_CALLS_TREE_TX = [ TX_FIELD('calls', FIELD_TYPES.rlpBinary) ] +const CHANNELS_TREE_TX = [ + ...BASE_TX, + TX_FIELD('channels', FIELD_TYPES.rlpBinary) +] + +const NAMESERVICE_TREE_TX = [ + ...BASE_TX, + TX_FIELD('mtree', FIELD_TYPES.rlpBinary) +] + +const ORACLES_TREE_TX = [ + ...BASE_TX, + TX_FIELD('otree', FIELD_TYPES.rlpBinary) +] + +const ACCOUNTS_TREE_TX = [ + ...BASE_TX, + TX_FIELD('accounts', FIELD_TYPES.rlpBinary) +] + export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.account]: TX_SCHEMA_FIELD(ACCOUNT_TX, OBJECT_TAG_ACCOUNT), [TX_TYPE.signed]: TX_SCHEMA_FIELD(SIGNED_TX, OBJECT_TAG_SIGNED_TRANSACTION), @@ -673,7 +705,11 @@ export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.merklePatriciaTree]: TX_SCHEMA_FIELD(MERKLE_PATRICIA_TREE_TX, OBJECT_TAG_MERKLE_PATRICIA_TREE), [TX_TYPE.merklePatriciaTreeValue]: TX_SCHEMA_FIELD(MERKLE_PATRICIA_TREE_VALUE_TX, OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE), [TX_TYPE.contractsTree]: TX_SCHEMA_FIELD(CONTRACTS_TREE_TX, OBJECT_TAG_CONTRACTS_TREE), - [TX_TYPE.contractCallsTree]: TX_SCHEMA_FIELD(CONTRACT_CALLS_TREE_TX, OBJECT_TAG_CONTRACT_CALLS_TREE) + [TX_TYPE.contractCallsTree]: TX_SCHEMA_FIELD(CONTRACT_CALLS_TREE_TX, OBJECT_TAG_CONTRACT_CALLS_TREE), + [TX_TYPE.channelsTree]: TX_SCHEMA_FIELD(CHANNELS_TREE_TX, OBJECT_TAG_CHANNELS_TREE), + [TX_TYPE.nameserviceTree]: TX_SCHEMA_FIELD(NAMESERVICE_TREE_TX, OBJECT_TAG_NAMESERVICE_TREE), + [TX_TYPE.oraclesTree]: TX_SCHEMA_FIELD(ORACLES_TREE_TX, OBJECT_TAG_ORACLES_TREE), + [TX_TYPE.accountsTree]: TX_SCHEMA_FIELD(ACCOUNTS_TREE_TX, OBJECT_TAG_ACCOUNTS_TREE) } export const TX_DESERIALIZATION_SCHEMA = { @@ -712,7 +748,11 @@ export const TX_DESERIALIZATION_SCHEMA = { [OBJECT_TAG_MERKLE_PATRICIA_TREE]: TX_SCHEMA_FIELD(MERKLE_PATRICIA_TREE_TX, OBJECT_TAG_MERKLE_PATRICIA_TREE), [OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE]: TX_SCHEMA_FIELD(MERKLE_PATRICIA_TREE_VALUE_TX, OBJECT_TAG_MERKLE_PATRICIA_TREE_VALUE), [OBJECT_TAG_CONTRACTS_TREE]: TX_SCHEMA_FIELD(CONTRACTS_TREE_TX, OBJECT_TAG_CONTRACTS_TREE), - [OBJECT_TAG_CONTRACT_CALLS_TREE]: TX_SCHEMA_FIELD(CONTRACT_CALLS_TREE_TX, OBJECT_TAG_CONTRACT_CALLS_TREE) + [OBJECT_TAG_CONTRACT_CALLS_TREE]: TX_SCHEMA_FIELD(CONTRACT_CALLS_TREE_TX, OBJECT_TAG_CONTRACT_CALLS_TREE), + [OBJECT_TAG_CHANNELS_TREE]: TX_SCHEMA_FIELD(CHANNELS_TREE_TX, OBJECT_TAG_CHANNELS_TREE), + [OBJECT_TAG_NAMESERVICE_TREE]: TX_SCHEMA_FIELD(NAMESERVICE_TREE_TX, OBJECT_TAG_NAMESERVICE_TREE), + [OBJECT_TAG_ORACLES_TREE]: TX_SCHEMA_FIELD(ORACLES_TREE_TX, OBJECT_TAG_ORACLES_TREE), + [OBJECT_TAG_ACCOUNTS_TREE]: TX_SCHEMA_FIELD(ACCOUNTS_TREE_TX, OBJECT_TAG_ACCOUNTS_TREE) } // VERIFICATION SCHEMA From c86bd4bcdbdaced5124b256d88cb69124c84ff58 Mon Sep 17 00:00:00 2001 From: Michal Powaga Date: Fri, 12 Apr 2019 18:51:56 +0100 Subject: [PATCH 21/21] Add channel snapshot solo tx serialization --- es/tx/builder/schema.js | 15 +++++++++++++++ es/tx/tx.js | 27 +++++++++++++++++++++++++++ test/integration/channel.js | 9 +++++++++ 3 files changed, 51 insertions(+) diff --git a/es/tx/builder/schema.js b/es/tx/builder/schema.js index a612449360..9d5d2feb14 100644 --- a/es/tx/builder/schema.js +++ b/es/tx/builder/schema.js @@ -49,6 +49,7 @@ const OBJECT_TAG_CHANNEL_SLASH_TX = 55 const OBJECT_TAG_CHANNEL_SETTLE_TX = 56 const OBJECT_TAG_CHANNEL_OFFCHAIN_TX = 57 const OBJECT_TAG_CHANNEL = 58 +const OBJECT_TAG_CHANNEL_SNAPSHOT_SOLO_TX = 59 const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX = 570 const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX = 571 const OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX = 572 @@ -133,6 +134,7 @@ export const TX_TYPE = { channelSettle: 'channelSettle', channelOffChain: 'channelOffChain', channel: 'channel', + channelSnapshotSolo: 'channelSnapshotSolo', channelOffChainUpdateTransfer: 'channelOffChainUpdateTransfer', channelOffChainUpdateDeposit: 'channelOffChainUpdateDeposit', channelOffChainUpdateWithdrawal: 'channelOffChainUpdateWithdrawal', @@ -180,6 +182,7 @@ export const OBJECT_ID_TX_TYPE = { [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_TYPE.channelSettle, [OBJECT_TAG_CHANNEL_OFFCHAIN_TX]: TX_TYPE.channelOffChain, [OBJECT_TAG_CHANNEL]: TX_TYPE.channel, + [OBJECT_TAG_CHANNEL_SNAPSHOT_SOLO_TX]: TX_TYPE.channelSnapshotSolo, [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX]: TX_TYPE.channelOffChainUpdateTransfer, [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX]: TX_TYPE.channelOffChainUpdateDeposit, [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX]: TX_TYPE.channelOffChainUpdateWithdrawal, @@ -568,6 +571,16 @@ const CHANNEL_TX = [ TX_FIELD('lockedUntil', FIELD_TYPES.int) ] +const CHANNEL_SNAPSHOT_SOLO_TX = [ + ...BASE_TX, + TX_FIELD('channelId', FIELD_TYPES.id, 'ch'), + TX_FIELD('fromId', FIELD_TYPES.id, 'ak'), + TX_FIELD('payload', FIELD_TYPES.binary, 'tx'), + TX_FIELD('ttl', FIELD_TYPES.int), + TX_FIELD('fee', FIELD_TYPES.int), + TX_FIELD('nonce', FIELD_TYPES.int) +] + const CHANNEL_OFFCHAIN_CREATE_CONTRACT_TX = [ ...BASE_TX, TX_FIELD('owner', FIELD_TYPES.id, 'ak'), @@ -695,6 +708,7 @@ export const TX_SERIALIZATION_SCHEMA = { [TX_TYPE.channelSettle]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX), [TX_TYPE.channelOffChain]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_TX), [TX_TYPE.channel]: TX_SCHEMA_FIELD(CHANNEL_TX, OBJECT_TAG_CHANNEL), + [TX_TYPE.channelSnapshotSolo]: TX_SCHEMA_FIELD(CHANNEL_SNAPSHOT_SOLO_TX, OBJECT_TAG_CHANNEL_SNAPSHOT_SOLO_TX), [TX_TYPE.channelOffChainUpdateTransfer]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX), [TX_TYPE.channelOffChainUpdateDeposit]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX), [TX_TYPE.channelOffChainUpdateWithdrawal]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX), @@ -738,6 +752,7 @@ export const TX_DESERIALIZATION_SCHEMA = { [OBJECT_TAG_CHANNEL_SETTLE_TX]: TX_SCHEMA_FIELD(CHANNEL_SETTLE_TX, OBJECT_TAG_CHANNEL_SETTLE_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_TX), [OBJECT_TAG_CHANNEL]: TX_SCHEMA_FIELD(CHANNEL_TX, OBJECT_TAG_CHANNEL), + [OBJECT_TAG_CHANNEL_SNAPSHOT_SOLO_TX]: TX_SCHEMA_FIELD(CHANNEL_SNAPSHOT_SOLO_TX, OBJECT_TAG_CHANNEL_SNAPSHOT_SOLO_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_TRANSFER_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_DEPOSIT_TX), [OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX]: TX_SCHEMA_FIELD(CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX, OBJECT_TAG_CHANNEL_OFFCHAIN_UPDATE_WITHDRAWAL_TX), diff --git a/es/tx/tx.js b/es/tx/tx.js index 9a09c3eb77..86de6c57d7 100644 --- a/es/tx/tx.js +++ b/es/tx/tx.js @@ -330,6 +330,32 @@ async function channelSettleTx ({ channelId, fromId, initiatorAmountFinal, respo return tx } +async function channelSnapshotSoloTx ({ channelId, fromId, payload }) { + // Calculate fee, get absolute ttl (ttl + height), get account nonce + const { fee, ttl, nonce } = await this.prepareTxParams(TX_TYPE.channelSnapshotSolo, { senderId: fromId, ...R.head(arguments), payload }) + + // Build transaction using sdk (if nativeMode) or build on `AETERNITY NODE` side + const { tx } = this.nativeMode + ? buildTx(R.merge(R.head(arguments), { + channelId, + fromId, + payload, + ttl, + fee, + nonce + }), TX_TYPE.channelSnapshotSolo) + : await this.api.postChannelSnapshotSolo(R.merge(R.head(arguments), { + channelId, + fromId, + payload, + ttl, + fee: parseInt(fee), + nonce + })) + + return tx +} + /** * Compute the absolute ttl by adding the ttl to the current height of the chain * @@ -422,6 +448,7 @@ const Transaction = Node.compose(Tx, { channelCloseSoloTx, channelSlashTx, channelSettleTx, + channelSnapshotSoloTx, getAccountNonce } }) diff --git a/test/integration/channel.js b/test/integration/channel.js index d68fe44ebe..f787cbc759 100644 --- a/test/integration/channel.js +++ b/test/integration/channel.js @@ -648,6 +648,15 @@ describe('Channel', function () { // TODO: contractState deserialization }) + it('can post snapshot solo transaction', async () => { + const snapshotSoloTx = await initiator.channelSnapshotSoloTx({ + channelId: initiatorCh.id(), + fromId: await initiator.address(), + payload: (await initiatorCh.state()).signedTx + }) + await initiator.sendTransaction(await initiator.signTransaction(snapshotSoloTx), { waitMined: true }) + }) + describe('throws errors', function () { before(async function () { initiatorCh = await Channel({