/*
* @Author: amitshah
* @Date: 2018-04-17 01:15:31
* @Last Modified by: amitshah
* @Last Modified time: 2018-04-28 23:33:08
*/
/** @namespace channel */
const message = require('./message');
const channelState = require('./channelState');
const util = require('ethereumjs-util');
//Transfers apply state mutations to the channel object. Once a transfer is verified
//we apply it to the Channel
/** @memberof channel */
const CHANNEL_STATE_IS_OPENING = 'opening';
/** @memberof channel */
const CHANNEL_STATE_IS_CLOSING = 'closing';
/** @memberof channel */
const CHANNEL_STATE_IS_SETTLING = 'settling';
/** @memberof channel */
const CHANNEL_STATE_CLOSED = 'closed';
/** @memberof channel */
const CHANNEL_STATE_OPEN = 'opened';
/** @memberof channel */
const CHANNEL_STATE_SETTLED = 'settled';
/** @memberof channel */
SETTLE_TIMEOUT = new util.BN(100);
//the minimum amount of time we need from the expiration of a lock to safely unlock
//this property should be negotiable by the users based on their level of conservantiveness
//in addition to expection of settled locks
/** @memberof channel */
REVEAL_TIMEOUT = new util.BN(15);
/** @class Channel represents the states between two participants. State synchronization occurs against channel endpoints.
* Channels rely on monotonically increasing simplex channels to track net value transfer flux. Rather then leveraging Poon-Dryja style
* channels for value transfer with off-chain hashed time lock contracts, we prefer the Raiden Network style simplex channels. The advantage of Raiden
* style channels is the counterparties are not punished for providing invalid or out of date lock proofs to the block-chain. From a game theoretic
* perspective, since the value is monotonically increasing, a rational actor will attempt to send the most up to date and relevant proof to the blockchain.
* However; this is only effective for monotonically increasing
* value transfers. For general state transfers where the value may vary wildly, we will subclass our LockedTransfer class towards Poon-Dryja implementation.
* This scheme requires a deposit from both parties (i.e a COMMIT transaction) which is used as punishment for publishing invalidated hashLocks on the blockchain.
* This is enforced simply as with every new state transfer, a new secret is generated and the old secret is revealed by the party creating the updates.
* Much care must be given to the order of the operations and the counterparties actions in any of the implementations. Our design decision relies
* on mass consumer adoption; i.e. it is likely users will lose proof messages (lost phones, forgotten passwords, hardware failures), and they shouldn't
* be further punished by the blockchain as they already have lost value due to a stale proof.
* @see https://en.bitcoin.it/wiki/Payment_channels for further details of implementation strategies.
* @property {ChannelState} peerState - peer endpoint state
* @property {ChannelState} myState - my endpoint state
* @property {Buffer} channelAddress - the Ethereum NettingChannel Contract Address for this channel
* @property {BN} openedBlock - the block the channel was opened
* @property {BN} closedBlock=null
* @property {BN} settledBlock=null
* @property {BN} issuedCloseBlock=null - the block the channel issuedCloseBlock, null if you never issue
* @property {BN} issuedTransferUpdateBlock=null
* @property {BN} issuedSettleBlock=null
* @property {BN} updatedProofBlock=null - the block you sent your proof message to the on-chain netting channel, null if your partner never send you value transfers
* @property {Object.<string,int>} withdrawnLocks - the state of the on-chain withdraw proof */
class Channel{
/**
* @constructor
* @param {ChannelState} peerState - The initialized ChannelState object representing a peer.
* @param {ChannelState} myState - The initialized ChannelState object representing my state.
* @param {Bytes} channelAddress - The on chain netting channel ethereum contract address.
* @param {BN} currentBlock - The current block number on ethereum.
*/
constructor(peerState,myState,channelAddress,currentBlock){
this.peerState = peerState; //channelState.ChannelStateSync
this.myState = myState;//channelState.ChannelStateSync
this.channelAddress = channelAddress || message.EMPTY_20BYTE_BUFFER;
this.openedBlock = message.TO_BN(currentBlock);
this.issuedCloseBlock = null;
this.issuedTransferUpdateBlock = null;
this.issuedSettleBlock = null;
this.closedBlock = null;
this.settledBlock = null;
this.updatedProofBlock = null;
this.withdrawnLocks = {};
}
/**
* The amount of funds that can be sent from -> to in the payment channel at a particular block.
* The block is important as locks expire those funds are made available again
* @param {ChannelState} from
* @param {ChannelState} to
* @param {BN} currentBlock - The current block number
* @returns {BN}
*/
transferrableFromTo(from,to,currentBlock){
var safeBlock = null;
if(currentBlock){
safeBlock = currentBlock.add(REVEAL_TIMEOUT);
}
return from.depositBalance.
sub((from.transferredAmount.add(from.lockedAmount(safeBlock)).add(from.unlockedAmount())))
.add(to.transferredAmount.add(to.unlockedAmount()));
}
/** determine which absolute block number the settlement period ends
* @param {BN} currentBlock
* @returns {BN}
*/
getChannelExpirationBlock(currentBlock){
if(this.closedBlock){
return this.closedBlock.add(SETTLE_TIMEOUT);
}else{
return currentBlock.add(SETTLE_TIMEOUT);
}
}
/** @property {string} - return the current channel state */
get state(){
if(this.settledBlock){
return CHANNEL_STATE_SETTLED;
}
else if(this.issuedSettleBlock) {
return CHANNEL_STATE_IS_SETTLING;
}
else if(this.closedBlock){
return CHANNEL_STATE_CLOSED;
}else if(this.issuedCloseBlock){
return CHANNEL_STATE_IS_CLOSING;
} else {
return CHANNEL_STATE_OPEN;
}
}
/** @returns {bool} returns true iff the channel is open*/
isOpen(){
return this.state === CHANNEL_STATE_OPEN;
}
/** @param {message.RevealSecret} revealSecret
* @returns {bool} - true if applied
* @throws "Invalid Message: Expected RevealSecret"
* @throws "Invalid Secret: Unknown secret revealed"
*/
handleRevealSecret(revealSecret){
if(!revealSecret instanceof message.RevealSecret){
throw new Error("Invalid Message: Expected RevealSecret");
};
//TODO: we dont care where it comes from?
//var from = null;
// if(this.myState.address.compare(revealSecret.from)===0){
// from = this.myState;
// }else if(this.peerState.address.compare(revealSecret.from)===0)
// {
// from = this.peerState;
// }
// if(!from){throw new Error("Invalid RevealSecret: Unknown secret sent")};
var myLock = this.myState.getLockFromSecret(revealSecret.secret);
var peerLock = this.peerState.getLockFromSecret(revealSecret.secret);
if(!myLock && !peerLock){
throw new Error("Invalid Secret: Unknown secret revealed");
}
if(myLock){
this.myState.applyRevealSecret(revealSecret);
}
if(peerLock){
this.peerState.applyRevealSecret(revealSecret);
}
return true;
}
/** @param {(message.DirectTransfer|message.LockedTransfer)} transfer
* @param {BN} currentBlock
* @throws "Invalid transfer: cannot update a closing channel"
* @throws "Invalid Transfer: unknown from"
*/
handleTransfer(transfer,currentBlock){
//check the direction of data flow
if(!this.isOpen()){
throw new Error("Invalid transfer: cannot update a closing channel");
}
if(this.myState.address.compare(transfer.from) ==0){
this.handleTransferFromTo(this.myState,this.peerState,transfer,currentBlock);
}else if(this.peerState.address.compare(transfer.from) ==0){
this.handleTransferFromTo(this.peerState,this.myState,transfer,currentBlock);
}else{
throw new Error("Invalid Transfer: unknown from");
}
}
/** process a transfer in the direction of from > to channelState
* @param {ChannelState} from - transfer originator
* @param {ChannelState} to - transfer recipient
* @param {(message.DirectTransfer|message.LockedTransfer)} transfer
* @param {BN} currentBlock
* @throws "Invalid Transfer Type"
* @throws "Invalid Channel Address: channel address mismatch"
* @throws "Invalid nonce: Nonce must be incremented by 1"
* @throws "Invalid Lock: Lock registered previously"
* @throws "Invalid LocksRoot for LockedTransfer"
* @throws "Invalid Lock: Lock amount must be greater than 0"
* @throws "Invalid SecretToProof: unknown secret"
* @throws "Invalid LocksRoot for SecretToProof:..."
* @throws "Invalid transferredAmount: must be monotonically increasing value"
* @throws "Invalid transferredAmount: SecretToProof does not provide expected lock amount"
* @throws "Invalid transferredAmount: Insufficient Balance:..."
* @returns {bool} - true if transfer applied to channelState
*/
handleTransferFromTo(from,to,transfer,currentBlock){
if(!transfer instanceof message.ProofMessage){
throw new Error("Invalid Transfer Type");
}
var proof = transfer.toProof();
if(proof.channelAddress.compare(this.channelAddress)!==0){
throw new Error("Invalid Channel Address: channel address mismatch");
}
if(!proof.nonce.eq(from.nonce.add(new util.BN(1)))){
throw new Error("Invalid nonce: Nonce must be incremented by 1");
}
//Validate LocksRoot
if(transfer instanceof message.LockedTransfer){
var lock = transfer.lock;
if(from.containsLock(lock)){
throw new Error("Invalid Lock: Lock registered previously");
}
var mtValidate = from._computeMerkleTreeWithHashlock(lock);
if(mtValidate.getRoot().compare(proof.locksRoot)!==0){
throw new Error("Invalid LocksRoot for LockedTransfer");
}
//validate lock as well
if(lock.amount.lte(new util.BN(0))){
throw new Error("Invalid Lock: Lock amount must be greater than 0");
}
//unfortunately we must handle all lock requests because then the state roots will
//be unsynched. What we can do instead is if the lock is outside our comfort zone
//we simply dont make a RequestSecret to the initiator. if we are in a mediated transfer
//dont forward message, but alteast the locksRoots are synced
// var expirationBlock = this.getChannelExpirationBlock(currentBlock);
// //= currentBlock.add(revealTimeout)<= expirationBlock <= currentBlock.add(SETTLE_TIMEOUT)
// if(lock.expiration.lt(currentBlock.add(REVEAL_TIMEOUT)) || lock.expiration.gt(expirationBlock)){
// throw new Error("Invalid Lock Expiration: currentBlock+ this.REVEAL_TIMEOUT < Lock expiration < this.SETTLE_TIMEOUT ");
// }
}else if(transfer instanceof message.SecretToProof){
//TODO: dont try to retreive the lock, just calculate the hash and send in
//we do this twice thats why
//If we have a secretToProof for an expired lock, we dont care, as long as
//the lock exists we can take on the secretToProof
var lock = from.getLockFromSecret(transfer.secret);
if(!lock){
throw new Error("Invalid SecretToProof: unknown secret");
}
var mtValidate = from._computeMerkleTreeWithoutHashlock(lock);
if(mtValidate.getRoot().compare(proof.locksRoot)!==0){
throw new Error("Invalid LocksRoot for SecretToProof:"+mtValidate.getRoot().toString('hex')+"!="+proof.locksRoot.toString('hex'));
}
}else if(from.merkleTree.getRoot().compare(proof.locksRoot) !==0){
throw new Error("Invalid LocksRoot for Transfer");
}
//validate transferredAmount
if(proof.transferredAmount.lt(from.transferredAmount)){
throw new Error("Invalid transferredAmount: must be monotonically increasing value");
}
var transferrable = this.transferrableFromTo(from,to,currentBlock);
if(transfer instanceof message.SecretToProof){
var lock = from.getLockFromSecret(transfer.secret);//returns null if lock is not present
if(!lock || (proof.transferredAmount.lt(from.transferredAmount.add(lock.amount)))){
throw new Error("Invalid transferredAmount: SecretToProof does not provide expected lock amount");
};
//because we are removing the lock and adding it to transferred amount, we have access to the remaining funds
//IMPORTANT CHECK, or else if we sent a lock transfer greater then our remaining balance, we could never unlock with a secret proof
transferrable = transferrable.add(lock.amount);
}
//fix
//if the sent delta between messages is greater than the total transferrable amount (i.e. net value flux)
if(proof.transferredAmount.sub(from.transferredAmount).gt(transferrable)){
throw new Error("Invalid transferredAmount: Insufficient Balance:"+proof.transferredAmount.toString()+" > "+transferrable.toString());
}
if(transfer instanceof message.LockedTransfer){
from.applyLockedTransfer(transfer);
}else if(transfer instanceof message.DirectTransfer){
from.applyDirectTransfer(transfer);
}if(transfer instanceof message.SecretToProof){
from.applySecretToProof(transfer);
}
//validate all the values of a transfer prior to applying it to the StateSync
return true;
}
/** @returns {BN} incremented nonce */
incrementedNonce(){
return this.myState.nonce.add(new util.BN(1));
}
/** create a locked transfer from myState for peerState
* @param {BN} msgID
* @param {Buffer} hashLock - the keccak256 hash of the secret
* @param {BN} amount
* @param {BN} expirationBlock
* @param {BN} currentBlock
* @returns message.LockedTransfer
* @throws "Insufficient funds: lock amount must be less than or equal to transferrable amount"
*/
createLockedTransfer(msgID,hashLock,amount,expirationBlock,currentBlock){
var transferrable = this.transferrableFromTo(this.myState,this.peerState,currentBlock);
if(amount.lte(new util.BN(0)) || transferrable.lt(amount)){
throw new Error("Insufficient funds: lock amount must be less than or equal to transferrable amount");
}
var lock = new message.Lock({amount:amount,expiration:expirationBlock, hashLock:hashLock})
var lockedTransfer = new message.LockedTransfer({
msgID:msgID,
nonce: this.incrementedNonce(),
channelAddress: this.channelAddress,
transferredAmount:this.myState.transferredAmount,
to:this.peerState.address,
locksRoot:this.myState._computeMerkleTreeWithHashlock(lock).getRoot(),
lock:lock
});
return lockedTransfer;
}
/** create a direct transfer from myState to peerState
* @param {BN} msgID
* @param {BN} amount
* @returns message.DirectTransfer
* @throws "Insufficient funds: direct transfer cannot be completed:..."
*/
createDirectTransfer(msgID,transferredAmount){
var transferrable = this.transferrableFromTo(this.myState, this.peerState);
if(transferredAmount.lte(new util.BN(0)) ||
transferredAmount.lte(this.myState.transferredAmount) ||
transferredAmount.gt(transferrable)){
throw new Error("Insufficient funds: direct transfer cannot be completed:"
+ transferredAmount.toString()+" - "+this.myState.transferredAmount.toString() +" > "
+ transferrable.toString(10));
}
var directTransfer = new message.DirectTransfer({
msgID:msgID,
nonce: this.incrementedNonce(),
channelAddress: this.channelAddress,
transferredAmount:transferredAmount,
to:this.peerState.address,
locksRoot:this.myState.merkleTree.getRoot()
});
return directTransfer;
}
/** create a mediated transfer from myState to target using the peerState as a mediator and is set as the to address.
* This holds if there exists a route in the
* state channel network between myState and target through the peer
* @param {BN} msgID
* @param {Buffer} hashLock - the keccak256 hash of the secret
* @param {BN} amount
* @param {BN} expirationBlock
* @param {BN} expirationBlock
* @param {Buffer} target - the intended recipient of the locked transfer. This target node will make the RevealSecret request
* direction to the initiator
* @param {Buffer} initiator - myState ethereum address
* @param {BN} currentBlock
* @returns message.MediatedTransfer
*/
createMediatedTransfer(msgID,hashLock,amount,expiration,target,initiator,currentBlock){
var lockedTransfer = this.createLockedTransfer(msgID,hashLock,amount,expiration,currentBlock);
var mediatedTransfer = new message.MediatedTransfer(
Object.assign(
{
target:target,
initiator:initiator
},lockedTransfer));
return mediatedTransfer;
}
/** Move an openLocks amount to the transferredAmount and remove from merkletree, this can increase channel longevity
* as openLocks will require on-chain withdrawals without this mechanism.
*@param {BN} msgID
*@param {Buffer} secret
*@returns {message.SecretToProof}
*/
createSecretToProof(msgID,secret){
var lock = this.myState.getLockFromSecret(secret);
if(!lock){
console.log(Object.keys(this.myState.openLocks).map(function (l) {
console.log("openLock:"+l);
}));
throw new Error("Invalid Secret: lock does not exist for secret:"+secret);
}
var mt = this.myState._computeMerkleTreeWithoutHashlock(lock);
var transferredAmount = this.myState.transferredAmount.add(lock.amount);
var secretToProof = new message.SecretToProof({
msgID:msgID,
nonce:this.incrementedNonce(),
channelAddress: this.channelAddress,
transferredAmount:transferredAmount,
to:this.peerState.address,
locksRoot:mt.getRoot(),
secret:secret
})
return secretToProof;
}
/** handle a block update
* @param {BN} currentBlock
* @returns {string[]} - GOT.* events to be processed
* @see Engine.handleEvent
*/
onBlock(currentBlock){
//we use to auto issue settle but now we leave it to the user.
var events =[]
if(this.canIssueSettle(currentBlock)){
events.push(["GOT.issueSettle", this.channelAddress]);
}
return events;
// var earliestLockExpiration = this.peerState.minOpenLockExpiration;
// if(earliestLockExpiration.sub(revealTimeout).gte(currentBlock)){
// this.handleClose(this.myState.address,currentBlock);
// return false;//We have to close this channel
// }
}
/** @param {BN} currentBlock
@returns {bool}
*/
canIssueSettle(currentBlock){
return (this.closedBlock &&
currentBlock.gt(this.closedBlock.add(SETTLE_TIMEOUT)));
}
issueSettle(currentBlock){
if(this.canIssueSettle(currentBlock)){
this.issuedSettleBlock = currentBlock;
}
return this.issuedSettleBlock;
}
issueClose(currentBlock){
if(!this.issuedCloseBlock && !this.closedBlock){
this.issuedCloseBlock = currentBlock;
return this.peerState.proof.signature ? this.peerState.proof : null;
}
throw new Error("Channel Error: In Closing State or Is Closed");
}
issueTransferUpdate(currentBlock){
if(!this.issuedCloseBlock){
this.issuedTransferUpdateBlock = currentBlock;
return this.peerState.proof.signature ? this.peerState.proof : null;
}
}
issueWithdrawPeerOpenLocks(currentBlock){
//TODO: Enable this with updated Test
// if(!this.updatedProofBlock){
// throw new Error("Channel Error: Cannot withdraw lock without updating proof to blockchain");
// }
var openLockProofs = this._withdrawPeerOpenLocks();
for(var i=0; i < openLockProofs.length; i++){
var openLock = openLockProofs[i].openLock;
var hashKey = util.addHexPrefix(openLock.hashLock.toString('hex'));
this.withdrawnLocks[hashKey] = currentBlock;
}
return openLockProofs;
}
/** Internal
@returns {channel.OpenLock}
*/
_withdrawPeerOpenLocks(){
//withdraw all open locks
var self = this;
var lockProofs = Object.values(this.peerState.openLocks).map(function (lock) {
try{
return new OpenLockProof({"openLock":lock,"merkleProof":self.peerState.generateLockProof(lock)});
}catch(err){
console.log(err);
return;
}
});
return lockProofs;
}
onChannelNewBalance(address,balance){
if(this.myState.address.compare(address) === 0){
this._handleDepositFrom(this.myState,balance);
}else if(this.peerState.address.compare(address)===0){
this._handleDepositFrom(this.peerState,balance);
}
}
_handleDepositFrom(from, depositAmount){
//deposit amount must be monotonically increasing
if(from.depositBalance.lt(depositAmount)){
from.depositBalance = depositAmount;
}else{
throw new Error("Invalid Deposit Amount: deposit must be monotonically increasing");
}
}
onChannelClose(closingAddress,block){
if(!this.closedBlock){
this.closedBlock = block;
if(this.issuedCloseBlock){
this.updatedProofBlock = block;
}
return true;
}else{
throw new Error("Channel Error: Channel Already Closed");
}
}
onChannelCloseError(){
if(!this.closedBlock){
this.issuedCloseBlock = null;
}
}
onTransferUpdated(nodeAddress,block){
if(!this.updatedProofBlock){
this.updatedProofBlock = block;
}
}
onTransferUpdatedError(){
if(!this.updatedProofBlock){
this.issuedTransferUpdateBlock = null;
this.updatedProofBlock = null;
}
}
onChannelSettled(block){
if(!this.settledBlock){
this.settledBlock = block;
}
}
onChannelSettledError(){
if(!this.settledBlock){
this.settledBlock = null;
this.issuedSettleBlock = null;
}
}
onChannelSecretRevealed(secret,receiverAddress,block){
var hashKey = util.addHexPrefix((util.sha3(secret)).toString('hex'));
if(this.withdrawnLocks.hasOwnProperty(hashKey)){
this.withdrawnLocks[hashKey] = block;
}
};
onChannelSecretRevealedError(secret){
var hashKey = util.addHexPrefix((util.sha3(secret)).toString('hex'));
this.withdrawnLocks[hashKey] = null;
};
onRefund(receiverAddress, amount){
}
}
/** @class encapsulate open lock proof for submission to blockchain
* @memberof channel
* @property {message.OpenLock} openLock
* @property {Buffer[]} merkleProof
*/
class OpenLockProof{
constructor(options){
this.openLock = options.openLock;
this.merkleProof = options.merkleProof;
}
encodeLock(){
//we dont want the secret appended to this encoding
return this.openLock.encode().slice(0,96);
}
}
module.exports = {
Channel,SETTLE_TIMEOUT,REVEAL_TIMEOUT,CHANNEL_STATE_IS_CLOSING,CHANNEL_STATE_IS_SETTLING, CHANNEL_STATE_IS_OPENING,
CHANNEL_STATE_OPEN, CHANNEL_STATE_CLOSED, CHANNEL_STATE_SETTLED,OpenLockProof
}