Skip to content

Commit ae4426e

Browse files
mpowaganduchak
authored andcommitted
feat(state channels): allow off chain updates to be cancelled with custom error code (#753)
1 parent ddc6611 commit ae4426e

File tree

2 files changed

+215
-21
lines changed

2 files changed

+215
-21
lines changed

es/channel/handlers.js

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ function encodeRlpTx (rlpBinary) {
3434
async function appendSignature (tx, signFn) {
3535
const { signatures, encodedTx } = unpackTx(tx).tx
3636
const result = await signFn(encodeRlpTx(encodedTx.rlpEncoded))
37-
if (result) {
37+
if (typeof result === 'string') {
3838
const { tx: signedTx, txType } = unpackTx(result)
3939
return encodeRlpTx(buildTx({
4040
signatures: signatures.concat(signedTx.signatures),
4141
encodedTx: signedTx.encodedTx.rlpEncoded
4242
}, txType).rlpEncoded)
4343
}
44+
return result
4445
}
4546

4647
function handleUnexpectedMessage (channel, message, state) {
@@ -209,8 +210,14 @@ export async function awaitingOffChainTx (channel, message, state) {
209210
const signedTx = await appendSignature(message.params.data.signed_tx, tx =>
210211
sign(tx, { updates: message.params.data.updates })
211212
)
212-
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { signed_tx: signedTx } })
213-
return { handler: awaitingOffChainUpdate, state }
213+
if (typeof signedTx === 'string') {
214+
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { signed_tx: signedTx } })
215+
return { handler: awaitingOffChainUpdate, state }
216+
}
217+
if (typeof signedTx === 'number') {
218+
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { error: signedTx } })
219+
return { handler: awaitingOffChainTx, state }
220+
}
214221
}
215222
if (message.method === 'channels.error') {
216223
state.reject(new Error(message.data.message))
@@ -227,6 +234,14 @@ export async function awaitingOffChainTx (channel, message, state) {
227234
}
228235
return { handler: channelOpen }
229236
}
237+
if (message.method === 'channels.conflict') {
238+
state.resolve({
239+
accepted: false,
240+
errorCode: message.params.data.error_code,
241+
errorMessage: message.params.data.error_msg
242+
})
243+
return { handler: channelOpen }
244+
}
230245
return handleUnexpectedMessage(channel, message, state)
231246
}
232247

@@ -237,7 +252,11 @@ export function awaitingOffChainUpdate (channel, message, state) {
237252
return { handler: channelOpen }
238253
}
239254
if (message.method === 'channels.conflict') {
240-
state.resolve({ accepted: false })
255+
state.resolve({
256+
accepted: false,
257+
errorCode: message.params.data.error_code,
258+
errorMessage: message.params.data.error_msg
259+
})
241260
return { handler: channelOpen }
242261
}
243262
if (message.error) {
@@ -262,10 +281,14 @@ export async function awaitingTxSignRequest (channel, message, state) {
262281
const signedTx = await appendSignature(message.params.data.signed_tx, tx =>
263282
options.get(channel).sign(tag, tx, { updates: message.params.data.updates })
264283
)
265-
if (signedTx) {
284+
if (typeof signedTx === 'string') {
266285
send(channel, { jsonrpc: '2.0', method: `channels.${tag}`, params: { signed_tx: signedTx } })
267286
return { handler: channelOpen }
268287
}
288+
if (typeof signedTx === 'number') {
289+
send(channel, { jsonrpc: '2.0', method: `channels.${tag}`, params: { error: signedTx } })
290+
return { handler: awaitingUpdateConflict }
291+
}
269292
}
270293
// soft-reject via competing update
271294
send(channel, {
@@ -338,8 +361,14 @@ export async function awaitingWithdrawTx (channel, message, state) {
338361
const signedTx = await appendSignature(message.params.data.signed_tx, tx =>
339362
sign(tx, { updates: message.params.data.updates })
340363
)
341-
send(channel, { jsonrpc: '2.0', method: 'channels.withdraw_tx', params: { signed_tx: signedTx } })
342-
return { handler: awaitingWithdrawCompletion, state }
364+
if (typeof signedTx === 'string') {
365+
send(channel, { jsonrpc: '2.0', method: 'channels.withdraw_tx', params: { signed_tx: signedTx } })
366+
return { handler: awaitingWithdrawCompletion, state }
367+
}
368+
if (typeof signedTx === 'number') {
369+
send(channel, { jsonrpc: '2.0', method: 'channels.withdraw_tx', params: { error: signedTx } })
370+
return { handler: awaitingWithdrawCompletion, state }
371+
}
343372
}
344373
return handleUnexpectedMessage(channel, message, state)
345374
}
@@ -369,7 +398,11 @@ export function awaitingWithdrawCompletion (channel, message, state) {
369398
return { handler: channelOpen }
370399
}
371400
if (message.method === 'channels.conflict') {
372-
state.resolve({ accepted: false })
401+
state.resolve({
402+
accepted: false,
403+
errorCode: message.params.data.error_code,
404+
errorMessage: message.params.data.error_msg
405+
})
373406
return { handler: channelOpen }
374407
}
375408
return handleUnexpectedMessage(channel, message, state)
@@ -386,8 +419,14 @@ export async function awaitingDepositTx (channel, message, state) {
386419
const signedTx = await appendSignature(message.params.data.signed_tx, tx =>
387420
sign(tx, { updates: message.params.data.updates })
388421
)
389-
send(channel, { jsonrpc: '2.0', method: 'channels.deposit_tx', params: { signed_tx: signedTx } })
390-
return { handler: awaitingDepositCompletion, state }
422+
if (typeof signedTx === 'string') {
423+
send(channel, { jsonrpc: '2.0', method: 'channels.deposit_tx', params: { signed_tx: signedTx } })
424+
return { handler: awaitingDepositCompletion, state }
425+
}
426+
if (typeof signedTx === 'number') {
427+
send(channel, { jsonrpc: '2.0', method: 'channels.deposit_tx', params: { error: signedTx } })
428+
return { handler: awaitingDepositCompletion, state }
429+
}
391430
}
392431
return handleUnexpectedMessage(channel, message, state)
393432
}
@@ -417,7 +456,11 @@ export function awaitingDepositCompletion (channel, message, state) {
417456
return { handler: channelOpen }
418457
}
419458
if (message.method === 'channels.conflict') {
420-
state.resolve({ accepted: false })
459+
state.resolve({
460+
accepted: false,
461+
errorCode: message.params.data.error_code,
462+
errorMessage: message.params.data.error_msg
463+
})
421464
return { handler: channelOpen }
422465
}
423466
return handleUnexpectedMessage(channel, message, state)
@@ -431,8 +474,14 @@ export async function awaitingNewContractTx (channel, message, state) {
431474
return { handler: awaitingNewContractCompletion, state }
432475
}
433476
const signedTx = await appendSignature(message.params.data.signed_tx, tx => state.sign(tx))
434-
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { signed_tx: signedTx } })
435-
return { handler: awaitingNewContractCompletion, state }
477+
if (typeof signedTx === 'string') {
478+
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { signed_tx: signedTx } })
479+
return { handler: awaitingNewContractCompletion, state }
480+
}
481+
if (typeof signedTx === 'number') {
482+
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { error: signedTx } })
483+
return { handler: awaitingNewContractCompletion, state }
484+
}
436485
}
437486
return handleUnexpectedMessage(channel, message, state)
438487
}
@@ -453,7 +502,11 @@ export function awaitingNewContractCompletion (channel, message, state) {
453502
return { handler: channelOpen }
454503
}
455504
if (message.method === 'channels.conflict') {
456-
state.resolve({ accepted: false })
505+
state.resolve({
506+
accepted: false,
507+
errorCode: message.params.data.error_code,
508+
errorMessage: message.params.data.error_msg
509+
})
457510
return { handler: channelOpen }
458511
}
459512
return handleUnexpectedMessage(channel, message, state)
@@ -467,8 +520,14 @@ export async function awaitingCallContractUpdateTx (channel, message, state) {
467520
return { handler: awaitingCallContractCompletion, state }
468521
}
469522
const signedTx = await appendSignature(message.params.data.signed_tx, tx => state.sign(tx))
470-
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { signed_tx: signedTx } })
471-
return { handler: awaitingCallContractCompletion, state }
523+
if (typeof signedTx === 'string') {
524+
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { signed_tx: signedTx } })
525+
return { handler: awaitingCallContractCompletion, state }
526+
}
527+
if (typeof signedTx === 'number') {
528+
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { error: signedTx } })
529+
return { handler: awaitingCallContractCompletion, state }
530+
}
472531
}
473532
return handleUnexpectedMessage(channel, message, state)
474533
}
@@ -480,7 +539,11 @@ export function awaitingCallContractCompletion (channel, message, state) {
480539
return { handler: channelOpen }
481540
}
482541
if (message.method === 'channels.conflict') {
483-
state.resolve({ accepted: false })
542+
state.resolve({
543+
accepted: false,
544+
errorCode: message.params.data.error_code,
545+
errorMessage: message.params.data.error_msg
546+
})
484547
return { handler: channelOpen }
485548
}
486549
return handleUnexpectedMessage(channel, message, state)

test/integration/channel.js

Lines changed: 135 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ describe('Channel', function () {
5959
let callerNonce
6060
const initiatorSign = sinon.spy((tag, tx) => initiator.signTransaction(tx))
6161
const responderSign = sinon.spy((tag, tx) => {
62+
if (typeof responderShouldRejectUpdate === 'number') {
63+
return responderShouldRejectUpdate
64+
}
6265
if (responderShouldRejectUpdate) {
6366
return null
6467
}
@@ -241,6 +244,32 @@ describe('Channel', function () {
241244
})
242245
})
243246

247+
it('can abort update sign request', async () => {
248+
const errorCode = 12345
249+
const result = await initiatorCh.update(
250+
await initiator.address(),
251+
await responder.address(),
252+
100,
253+
() => errorCode
254+
)
255+
result.should.eql({ accepted: false, errorCode, errorMessage: 'user-defined' })
256+
})
257+
258+
it('can abort update with custom error code', async () => {
259+
responderShouldRejectUpdate = 1234
260+
const result = await initiatorCh.update(
261+
await initiator.address(),
262+
await responder.address(),
263+
100,
264+
tx => initiator.signTransaction(tx)
265+
)
266+
result.should.eql({
267+
accepted: false,
268+
errorCode: responderShouldRejectUpdate,
269+
errorMessage: 'user-defined'
270+
})
271+
})
272+
244273
it('can post bignumber update and accept', async () => {
245274
responderShouldRejectUpdate = false
246275
const sign = sinon.spy(initiator.signTransaction.bind(initiator))
@@ -423,7 +452,7 @@ describe('Channel', function () {
423452
sign,
424453
{ onOnChainTx, onOwnWithdrawLocked, onWithdrawLocked }
425454
)
426-
result.should.eql({ accepted: false })
455+
result.should.eql({ ...result, accepted: false })
427456
sinon.assert.notCalled(onOnChainTx)
428457
sinon.assert.notCalled(onOwnWithdrawLocked)
429458
sinon.assert.notCalled(onWithdrawLocked)
@@ -462,6 +491,28 @@ describe('Channel', function () {
462491
})
463492
})
464493

494+
it('can abort withdraw sign request', async () => {
495+
const errorCode = 12345
496+
const result = await initiatorCh.withdraw(
497+
100,
498+
() => errorCode
499+
)
500+
result.should.eql({ accepted: false, errorCode, errorMessage: 'user-defined' })
501+
})
502+
503+
it('can abort withdraw with custom error code', async () => {
504+
responderShouldRejectUpdate = 12345
505+
const result = await initiatorCh.withdraw(
506+
100,
507+
tx => initiator.signTransaction(tx)
508+
)
509+
result.should.eql({
510+
accepted: false,
511+
errorCode: responderShouldRejectUpdate,
512+
errorMessage: 'user-defined'
513+
})
514+
})
515+
465516
it('can request a deposit and accept', async () => {
466517
const sign = sinon.spy(initiator.signTransaction.bind(initiator))
467518
const amount = BigNumber('2e18')
@@ -526,7 +577,7 @@ describe('Channel', function () {
526577
sign,
527578
{ onOnChainTx, onOwnDepositLocked, onDepositLocked }
528579
)
529-
result.should.eql({ accepted: false })
580+
result.should.eql({ ...result, accepted: false })
530581
sinon.assert.notCalled(onOnChainTx)
531582
sinon.assert.notCalled(onOwnDepositLocked)
532583
sinon.assert.notCalled(onDepositLocked)
@@ -553,6 +604,28 @@ describe('Channel', function () {
553604
})
554605
})
555606

607+
it('can abort deposit sign request', async () => {
608+
const errorCode = 12345
609+
const result = await initiatorCh.deposit(
610+
100,
611+
() => errorCode
612+
)
613+
result.should.eql({ accepted: false, errorCode, errorMessage: 'user-defined' })
614+
})
615+
616+
it('can abort deposit with custom error code', async () => {
617+
responderShouldRejectUpdate = 12345
618+
const result = await initiatorCh.deposit(
619+
100,
620+
tx => initiator.signTransaction(tx)
621+
)
622+
result.should.eql({
623+
accepted: false,
624+
errorCode: responderShouldRejectUpdate,
625+
errorMessage: 'user-defined'
626+
})
627+
})
628+
556629
it('can close a channel', async () => {
557630
const sign = sinon.spy(initiator.signTransaction.bind(initiator))
558631
const result = await initiatorCh.shutdown(sign)
@@ -782,7 +855,39 @@ describe('Channel', function () {
782855
vmVersion: 4,
783856
abiVersion: 1
784857
}, async (tx) => initiator.signTransaction(tx))
785-
result.should.eql({ accepted: false })
858+
result.should.eql({ ...result, accepted: false })
859+
})
860+
861+
it('can abort contract sign request', async () => {
862+
const errorCode = 12345
863+
const code = await initiator.compileContractAPI(identityContract, { backend: 'aevm' })
864+
const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', [], { backend: 'aevm' })
865+
const result = await initiatorCh.createContract({
866+
code,
867+
callData,
868+
deposit: BigNumber('10e18'),
869+
vmVersion: 4,
870+
abiVersion: 1
871+
}, () => errorCode)
872+
result.should.eql({ accepted: false, errorCode, errorMessage: 'user-defined' })
873+
})
874+
875+
it('can abort contract with custom error code', async () => {
876+
responderShouldRejectUpdate = 12345
877+
const code = await initiator.compileContractAPI(identityContract, { backend: 'aevm' })
878+
const callData = await initiator.contractEncodeCallDataAPI(identityContract, 'init', [], { backend: 'aevm' })
879+
const result = await initiatorCh.createContract({
880+
code,
881+
callData,
882+
deposit: BigNumber('10e18'),
883+
vmVersion: 4,
884+
abiVersion: 1
885+
}, async (tx) => initiator.signTransaction(tx))
886+
result.should.eql({
887+
accepted: false,
888+
errorCode: responderShouldRejectUpdate,
889+
errorMessage: 'user-defined'
890+
})
786891
})
787892

788893
it('can call a contract and accept', async () => {
@@ -804,7 +909,33 @@ describe('Channel', function () {
804909
contract: contractAddress,
805910
abiVersion: 1
806911
}, async (tx) => initiator.signTransaction(tx))
807-
result.should.eql({ accepted: false })
912+
result.should.eql({ ...result, accepted: false })
913+
})
914+
915+
it('can abort contract call sign request', async () => {
916+
const errorCode = 12345
917+
const result = await initiatorCh.callContract({
918+
amount: 0,
919+
callData: await contractEncodeCall('main', ['42']),
920+
contract: contractAddress,
921+
abiVersion: 1
922+
}, () => errorCode)
923+
result.should.eql({ accepted: false, errorCode, errorMessage: 'user-defined' })
924+
})
925+
926+
it('can abort contract call with custom error code', async () => {
927+
responderShouldRejectUpdate = 12345
928+
const result = await initiatorCh.callContract({
929+
amount: 0,
930+
callData: await contractEncodeCall('main', ['42']),
931+
contract: contractAddress,
932+
abiVersion: 1
933+
}, async (tx) => initiator.signTransaction(tx))
934+
result.should.eql({
935+
accepted: false,
936+
errorCode: responderShouldRejectUpdate,
937+
errorMessage: 'user-defined'
938+
})
808939
})
809940

810941
it('can get contract call', async () => {

0 commit comments

Comments
 (0)