Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow counterparty to close channel before cool down period #675

Merged
merged 2 commits into from Nov 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/avado/releases.json
Expand Up @@ -564,4 +564,4 @@
"http://80.208.229.228:5001": "Mon, 23 Nov 2020 11:02:14 GMT"
}
}
}
}
15 changes: 10 additions & 5 deletions packages/core-ethereum/src/channel/channel.ts
Expand Up @@ -8,8 +8,6 @@ import { hash } from '../utils'

import type HoprEthereum from '..'

import { OnChainChannel } from './types'

class Channel implements IChannel {
private _signedChannel: SignedChannel
private _settlementWindow?: Moment
Expand All @@ -36,10 +34,17 @@ class Channel implements IChannel {
this.ticket = new TicketFactory(this)
}

private get onChainChannel(): Promise<OnChainChannel> {
return new Promise<OnChainChannel>(async (resolve, reject) => {
private get onChainChannel(): Promise<{
deposit: string
partyABalance: string
closureTime: string
stateCounter: string
closureByPartyA: boolean
}> {
return new Promise(async (resolve, reject) => {
try {
return resolve(await this.coreConnector.channel.getOnChainState(await this.channelId))
const channelId = await this.channelId
return resolve(this.coreConnector.channel.getOnChainState(channelId))
} catch (error) {
return reject(error)
}
Expand Down
5 changes: 2 additions & 3 deletions packages/core-ethereum/src/channel/index.ts
Expand Up @@ -23,7 +23,6 @@ import Channel from './channel'
import { Uint8ArrayE } from '../types/extended'

import { CHANNEL_STATES } from './constants'
import { OnChainChannel } from './types'
import { Log } from 'web3-core'
import { TicketStatic } from './ticket'

Expand Down Expand Up @@ -73,7 +72,7 @@ class ChannelFactory {
const channelId = new Hash(await getId(await this.coreConnector.account.address, counterparty))

const [onChain, offChain]: [boolean, boolean] = await Promise.all([
this.coreConnector.channel.getOnChainState(channelId).then((channel: OnChainChannel) => {
this.coreConnector.channel.getOnChainState(channelId).then((channel) => {
const state = Number(channel.stateCounter) % CHANNEL_STATES
return state === ChannelStatus.OPEN || state === ChannelStatus.PENDING
}),
Expand Down Expand Up @@ -322,7 +321,7 @@ class ChannelFactory {
return this.coreConnector.db.del(Buffer.from(this.coreConnector.dbKeys.Channel(counterparty)))
}

getOnChainState(channelId: Hash): Promise<OnChainChannel> {
getOnChainState(channelId: Hash) {
return this.coreConnector.hoprChannels.methods.channels(channelId.toHex()).call()
}

Expand Down
6 changes: 0 additions & 6 deletions packages/core-ethereum/src/channel/types.ts

This file was deleted.

17 changes: 14 additions & 3 deletions packages/ethereum/contracts/HoprChannels.sol
Expand Up @@ -37,6 +37,7 @@ contract HoprChannels is IERC777Recipient, ERC1820Implementer {
* stateCounter mod 10 == 2: open
* stateCounter mod 10 == 3: pending
*/
bool closureByPartyA; // channel closure was initiated by party A
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we should decrease the size of a previous entry in the struct to keep the total size of the struct to <= 64 bytes (2 words). I suggest to decrease the closureTime to uint32 which is going to be fine until Sunday, February 7, 2106 6:28:15 AM.


// setup ERC1820
Expand Down Expand Up @@ -307,7 +308,7 @@ contract HoprChannels is IERC777Recipient, ERC1820Implementer {
function initiateChannelClosure(address counterparty) external {
address initiator = msg.sender;

(, , Channel storage channel, ChannelStatus status) = getChannel(initiator, counterparty);
(address partyA, , Channel storage channel, ChannelStatus status) = getChannel(initiator, counterparty);

require(status == ChannelStatus.OPEN, "HoprChannels: channel must be 'OPEN'");

Expand All @@ -318,6 +319,10 @@ contract HoprChannels is IERC777Recipient, ERC1820Implementer {
require(channel.stateCounter + 1 < (1 << 24), "HoprChannels: Preventing stateCounter overflow");
channel.stateCounter += 1;

if (initiator == partyA) {
channel.closureByPartyA = true;
}

emitInitiatedChannelClosure(initiator, counterparty, channel.closureTime);
}

Expand All @@ -337,9 +342,14 @@ contract HoprChannels is IERC777Recipient, ERC1820Implementer {
);

require(channel.stateCounter + 7 < (1 << 24), "Preventing stateCounter overflow");

require(status == ChannelStatus.PENDING, "HoprChannels: channel must be 'PENDING'");
require(now >= uint256(channel.closureTime), "HoprChannels: 'closureTime' has not passed");

if (
channel.closureByPartyA && (initiator == partyA) ||
!channel.closureByPartyA && (initiator == partyB)
) {
require(now >= uint256(channel.closureTime), "HoprChannels: 'closureTime' has not passed");
}

// settle balances
if (channel.partyABalance > 0) {
Expand All @@ -356,6 +366,7 @@ contract HoprChannels is IERC777Recipient, ERC1820Implementer {
delete channel.deposit; // channel.deposit = 0
delete channel.partyABalance; // channel.partyABalance = 0
delete channel.closureTime; // channel.closureTime = 0
delete channel.closureByPartyA; // channel.closureByPartyA = false

// The state counter indicates the recycling generation and ensures that both parties are using the correct generation.
// Increase state counter so that we can re-use the same channel after it has been closed.
Expand Down
168 changes: 165 additions & 3 deletions packages/ethereum/test/HoprChannels.test.ts
Expand Up @@ -19,10 +19,11 @@ const formatChannel = (res: AsyncReturnType<HoprChannelsInstance['channels']>) =
deposit: res[0],
partyABalance: res[1],
closureTime: res[2],
stateCounter: res[3]
stateCounter: res[3],
closureByPartyA: res[4]
})

describe('HoprChannels', function () {
describe.only('HoprChannels', function () {
const partyAPrivKey = NODE_SEEDS[1]
const partyBPrivKey = NODE_SEEDS[0]
const depositAmount = web3.utils.toWei('1', 'ether')
Expand Down Expand Up @@ -979,7 +980,7 @@ describe('HoprChannels', function () {
)
})

it("should fail when 'claimChannelClosure' before closureTime", async function () {
it('should initiate channel closure by partyA', async function () {
const secretHashA = keccak256({
type: 'string',
value: 'partyA secret'
Expand Down Expand Up @@ -1011,6 +1012,167 @@ describe('HoprChannels', function () {
from: partyB
}
)

await hoprToken.send(
hoprChannels.address,
depositAmount,
web3.eth.abi.encodeParameters(['address', 'address'], [partyA, partyB])
)

await hoprChannels.openChannel(partyB, {
from: partyA
})

await hoprChannels.initiateChannelClosure(partyB, {
from: partyA
})

const channel = await hoprChannels.channels(getChannelId(partyA, partyB)).then(formatChannel)
expect(channel.stateCounter.toString()).to.be.equal('3', 'wrong stateCounter')
expect(channel.closureByPartyA).to.be.true
})

it('should initiate channel closure by partyB', async function () {
const secretHashA = keccak256({
type: 'string',
value: 'partyA secret'
}).slice(0, 56)

const pubKeyA = secp256k1.publicKeyCreate(stringToU8a(partyAPrivKey), false).slice(1)

await hoprChannels.init(
u8aToHex(pubKeyA.slice(0, 32), true),
u8aToHex(pubKeyA.slice(32, 64), true),
secretHashA,
{
from: partyA
}
)

const secretHashB = keccak256({
type: 'string',
value: 'partyA secret'
}).slice(0, 56)

const pubKeyB = secp256k1.publicKeyCreate(stringToU8a(partyBPrivKey), false).slice(1)

await hoprChannels.init(
u8aToHex(pubKeyB.slice(0, 32), true),
u8aToHex(pubKeyB.slice(32, 64), true),
secretHashB,
{
from: partyB
}
)

await hoprToken.send(
hoprChannels.address,
depositAmount,
web3.eth.abi.encodeParameters(['address', 'address'], [partyA, partyB])
)

await hoprChannels.openChannel(partyB, {
from: partyA
})

await hoprChannels.initiateChannelClosure(partyA, {
from: partyB
})

const channel = await hoprChannels.channels(getChannelId(partyA, partyB)).then(formatChannel)
expect(channel.stateCounter.toString()).to.be.equal('3', 'wrong stateCounter')
expect(channel.closureByPartyA).to.be.false
})

it("should 'claimChannelClosure' before 'closureTime' when partyA is initiator", async function () {
const secretHashA = keccak256({
type: 'string',
value: 'partyA secret'
}).slice(0, 56)

const pubKeyA = secp256k1.publicKeyCreate(stringToU8a(partyAPrivKey), false).slice(1)

await hoprChannels.init(
u8aToHex(pubKeyA.slice(0, 32), true),
u8aToHex(pubKeyA.slice(32, 64), true),
secretHashA,
{
from: partyA
}
)

const secretHashB = keccak256({
type: 'string',
value: 'partyA secret'
}).slice(0, 56)

const pubKeyB = secp256k1.publicKeyCreate(stringToU8a(partyBPrivKey), false).slice(1)

await hoprChannels.init(
u8aToHex(pubKeyB.slice(0, 32), true),
u8aToHex(pubKeyB.slice(32, 64), true),
secretHashB,
{
from: partyB
}
)

await hoprToken.send(
hoprChannels.address,
depositAmount,
web3.eth.abi.encodeParameters(['address', 'address'], [partyA, partyB])
)

await hoprChannels.openChannel(partyB, {
from: partyA
})

await hoprChannels.initiateChannelClosure(partyA, {
from: partyB
})

await hoprChannels.claimChannelClosure(partyB, {
from: partyA
})

const channel = await hoprChannels.channels(getChannelId(partyA, partyB)).then(formatChannel)
expect(channel.stateCounter.toString()).to.be.equal('10', 'wrong stateCounter')
expect(channel.closureByPartyA).to.be.false
})

it("should fail when 'claimChannelClosure' is called before 'closureTime'", async function () {
const secretHashA = keccak256({
type: 'string',
value: 'partyA secret'
}).slice(0, 56)

const pubKeyA = secp256k1.publicKeyCreate(stringToU8a(partyAPrivKey), false).slice(1)

await hoprChannels.init(
u8aToHex(pubKeyA.slice(0, 32), true),
u8aToHex(pubKeyA.slice(32, 64), true),
secretHashA,
{
from: partyA
}
)

const secretHashB = keccak256({
type: 'string',
value: 'partyA secret'
}).slice(0, 56)

const pubKeyB = secp256k1.publicKeyCreate(stringToU8a(partyBPrivKey), false).slice(1)

await hoprChannels.init(
u8aToHex(pubKeyB.slice(0, 32), true),
u8aToHex(pubKeyB.slice(32, 64), true),
secretHashB,
{
from: partyB
}
)

await hoprToken.send(
hoprChannels.address,
depositAmount,
Expand Down