src/main/generic/consensus/base/account/HashedTimeLockedContract.js
class HashedTimeLockedContract extends Contract {
/**
* @param {number} balance
* @param {Address} sender
* @param {Address} recipient
* @param {Hash} hashRoot
* @param {number} hashCount
* @param {number} timeout
* @param {number} totalAmount
*/
constructor(balance = 0, sender = Address.NULL, recipient = Address.NULL, hashRoot = Hash.NULL, hashCount = 1, timeout = 0, totalAmount = balance) {
super(Account.Type.HTLC, balance);
if (!(sender instanceof Address)) throw new Error('Malformed address');
if (!(recipient instanceof Address)) throw new Error('Malformed address');
if (!(hashRoot instanceof Hash)) throw new Error('Malformed address');
if (!NumberUtils.isUint8(hashCount) || hashCount === 0) throw new Error('Malformed hashCount');
if (!NumberUtils.isUint32(timeout)) throw new Error('Malformed timeout');
if (!NumberUtils.isUint64(totalAmount)) throw new Error('Malformed totalAmount');
/** @type {Address} */
this._sender = sender;
/** @type {Address} */
this._recipient = recipient;
/** @type {Hash} */
this._hashRoot = hashRoot;
/** @type {number} */
this._hashCount = hashCount;
/** @type {number} */
this._timeout = timeout;
/** @type {number} */
this._totalAmount = totalAmount;
}
/**
* @param {number} balance
* @param {number} blockHeight
* @param {Transaction} transaction
*/
static create(balance, blockHeight, transaction) {
const buf = new SerialBuffer(transaction.data);
const sender = Address.unserialize(buf);
const recipient = Address.unserialize(buf);
const hashAlgorithm = /** @type {Hash.Algorithm} */ buf.readUint8();
const hashRoot = Hash.unserialize(buf, hashAlgorithm);
const hashCount = buf.readUint8();
const timeout = buf.readUint32();
return new HashedTimeLockedContract(balance, sender, recipient, hashRoot, hashCount, timeout);
}
/**
* @param {SerialBuffer} buf
* @return {HashedTimeLockedContract}
*/
static unserialize(buf) {
const type = buf.readUint8();
if (type !== Account.Type.HTLC) throw new Error('Invalid account type');
const balance = buf.readUint64();
const sender = Address.unserialize(buf);
const recipient = Address.unserialize(buf);
const hashAlgorithm = /** @type {Hash.Algorithm} */ buf.readUint8();
const hashRoot = Hash.unserialize(buf, hashAlgorithm);
const hashCount = buf.readUint8();
const timeout = buf.readUint32();
const totalAmount = buf.readUint64();
return new HashedTimeLockedContract(balance, sender, recipient, hashRoot, hashCount, timeout, totalAmount);
}
/**
* @param {object} plain
*/
static fromPlain(plain) {
if (!plain) throw new Error('Invalid account');
return new HashedTimeLockedContract(plain.balance, Address.fromAny(plain.sender), Address.fromAny(plain.recipient), Hash.fromAny(plain.hashRoot), plain.hashCount, plain.timeout, plain.totalAmount);
}
/**
* Serialize this HTLC object into binary form.
* @param {?SerialBuffer} [buf] Buffer to write to.
* @return {SerialBuffer} Buffer from `buf` or newly generated one.
*/
serialize(buf) {
buf = buf || new SerialBuffer(this.serializedSize);
super.serialize(buf);
this._sender.serialize(buf);
this._recipient.serialize(buf);
buf.writeUint8(this._hashRoot.algorithm);
this._hashRoot.serialize(buf);
buf.writeUint8(this._hashCount);
buf.writeUint32(this._timeout);
buf.writeUint64(this._totalAmount);
return buf;
}
/**
* @return {number}
*/
get serializedSize() {
return super.serializedSize
+ this._sender.serializedSize
+ this._recipient.serializedSize
+ /*hashAlgorithm*/ 1
+ this._hashRoot.serializedSize
+ /*hashCount*/ 1
+ /*timeout*/ 4
+ /*totalAmount*/ 8;
}
/** @type {Address} */
get sender() {
return this._sender;
}
/** @type {Address} */
get recipient() {
return this._recipient;
}
/** @type {Hash} */
get hashRoot() {
return this._hashRoot;
}
/** @type {number} */
get hashCount() {
return this._hashCount;
}
/** @type {number} */
get timeout() {
return this._timeout;
}
/** @type {number} */
get totalAmount() {
return this._totalAmount;
}
toString() {
return `HashedTimeLockedContract{balance=${this._balance}, sender=${this._sender.toUserFriendlyAddress(false)}, recipient=${this._sender.toUserFriendlyAddress(false)}, amount=${this._totalAmount}/${this._hashCount}, timeout=${this._timeout}}`;
}
/**
* @returns {object}
*/
toPlain() {
const plain = super.toPlain();
plain.sender = this.sender.toPlain();
plain.recipient = this.recipient.toPlain();
plain.hashRoot = this.hashRoot.toPlain();
plain.hashCount = this.hashCount;
plain.timeout = this.timeout;
plain.totalAmount = this.totalAmount;
return plain;
}
/**
* Check if two Accounts are the same.
* @param {Account} o Object to compare with.
* @return {boolean} Set if both objects describe the same data.
*/
equals(o) {
return o instanceof HashedTimeLockedContract
&& this._type === o._type
&& this._balance === o._balance
&& this._sender.equals(o._sender)
&& this._recipient.equals(o._recipient)
&& this._hashRoot.equals(o._hashRoot)
&& this._hashCount === o._hashCount
&& this._timeout === o._timeout
&& this._totalAmount === o._totalAmount;
}
/**
* @param {Transaction} transaction
* @return {boolean}
*/
static verifyOutgoingTransaction(transaction) {
try {
const buf = new SerialBuffer(transaction.proof);
const type = buf.readUint8();
switch (type) {
case HashedTimeLockedContract.ProofType.REGULAR_TRANSFER: {
const hashAlgorithm = /** @type {Hash.Algorithm} */ buf.readUint8();
const hashDepth = buf.readUint8();
const hashRoot = Hash.unserialize(buf, hashAlgorithm);
let preImage = Hash.unserialize(buf, hashAlgorithm);
// Verify that the preImage hashed hashDepth times matches the _provided_ hashRoot.
for (let i = 0; i < hashDepth; ++i) {
preImage = Hash.compute(preImage.array, hashAlgorithm);
}
if (!hashRoot.equals(preImage)) {
return false;
}
// Signature proof of the HTLC recipient
if (!SignatureProof.unserialize(buf).verify(null, transaction.serializeContent())) {
return false;
}
break;
}
case HashedTimeLockedContract.ProofType.EARLY_RESOLVE: {
// Signature proof of the HTLC recipient
if (!SignatureProof.unserialize(buf).verify(null, transaction.serializeContent())) {
return false;
}
// Signature proof of the HTLC creator
if (!SignatureProof.unserialize(buf).verify(null, transaction.serializeContent())) {
return false;
}
break;
}
case HashedTimeLockedContract.ProofType.TIMEOUT_RESOLVE:
// Signature proof of the HTLC creator
if (!SignatureProof.unserialize(buf).verify(null, transaction.serializeContent())) {
return false;
}
break;
default:
return false;
}
// Reject overlong proof.
if (buf.readPos !== buf.byteLength) {
return false;
}
return true; // Accept
} catch (e) {
return false;
}
}
/**
* @param {Transaction} transaction
* @return {boolean}
*/
static verifyIncomingTransaction(transaction) {
try {
const buf = new SerialBuffer(transaction.data);
Address.unserialize(buf); // sender address
Address.unserialize(buf); // recipient address
const hashAlgorithm = /** @type {Hash.Algorithm} */ buf.readUint8();
Hash.unserialize(buf, hashAlgorithm);
buf.readUint8(); // hash count
buf.readUint32(); // timeout
// Blacklist Argon2 hash function.
if (hashAlgorithm === Hash.Algorithm.ARGON2D) {
return false;
}
if (buf.readPos !== buf.byteLength) {
return false;
}
return Contract.verifyIncomingTransaction(transaction);
} catch (e) {
return false;
}
}
/**
* @param {number} balance
* @return {Account|*}
*/
withBalance(balance) {
return new HashedTimeLockedContract(balance, this._sender, this._recipient, this._hashRoot, this._hashCount, this._timeout, this._totalAmount);
}
/**
* @param {Transaction} transaction
* @param {number} blockHeight
* @param {TransactionCache} transactionsCache
* @param {boolean} [revert]
* @return {Account|*}
*/
withOutgoingTransaction(transaction, blockHeight, transactionsCache, revert = false) {
const buf = new SerialBuffer(transaction.proof);
const type = buf.readUint8();
let minCap = 0;
switch (type) {
case HashedTimeLockedContract.ProofType.REGULAR_TRANSFER: {
// Check that the contract has not expired yet.
if (this._timeout < blockHeight) {
throw new Account.ProofError();
}
// Check that the provided hashRoot is correct.
const hashAlgorithm = /** @type {Hash.Algorithm} */ buf.readUint8();
const hashDepth = buf.readUint8();
const hashRoot = Hash.unserialize(buf, hashAlgorithm);
if (!hashRoot.equals(this._hashRoot)) {
throw new Account.ProofError();
}
// Ignore the preImage.
Hash.unserialize(buf, hashAlgorithm);
// Verify that the transaction is signed by the authorized recipient.
if (!SignatureProof.unserialize(buf).isSignedBy(this._recipient)) {
throw new Account.ProofError();
}
minCap = Math.max(0, Math.floor((1 - (hashDepth / this._hashCount)) * this._totalAmount));
break;
}
case HashedTimeLockedContract.ProofType.EARLY_RESOLVE: {
if (!SignatureProof.unserialize(buf).isSignedBy(this._recipient)) {
throw new Account.ProofError();
}
if (!SignatureProof.unserialize(buf).isSignedBy(this._sender)) {
throw new Account.ProofError();
}
break;
}
case HashedTimeLockedContract.ProofType.TIMEOUT_RESOLVE: {
if (this._timeout >= blockHeight) {
throw new Account.ProofError();
}
if (!SignatureProof.unserialize(buf).isSignedBy(this._sender)) {
throw new Account.ProofError();
}
break;
}
default:
throw new Account.ProofError();
}
if (!revert) {
const newBalance = this._balance - transaction.value - transaction.fee;
if (newBalance < minCap) {
throw new Account.BalanceError();
}
}
return super.withOutgoingTransaction(transaction, blockHeight, transactionsCache, revert);
}
/**
* @param {Transaction} transaction
* @param {number} blockHeight
* @param {boolean} [revert]
* @return {Account}
*/
withIncomingTransaction(transaction, blockHeight, revert = false) {
throw new Error('Illegal incoming transaction');
}
/**
* @param {Uint8Array} data
* @return {object}
*/
static dataToPlain(data) {
try {
const buf = new SerialBuffer(proof);
const sender = Address.unserialize(buf);
const recipient = Address.unserialize(buf);
const hashAlgorithm = /** @type {Hash.Algorithm} */ buf.readUint8();
const hashRoot = Hash.unserialize(buf, hashAlgorithm);
const hashCount = buf.readUint8();
const timeout = buf.readUint32();
return {
sender: sender.toPlain(),
recipient: recipient.toPlain(),
hashAlgorithm: Hash.Algorithm.toString(hashAlgorithm),
hashRoot: hashRoot.toPlain(),
hashCount,
timeout
};
} catch (e) {
return Account.dataToPlain(data);
}
}
/**
* @param {Uint8Array} proof
* @return {object}
*/
static proofToPlain(proof) {
try {
const buf = new SerialBuffer(proof);
const type = buf.readUint8();
switch (type) {
case HashedTimeLockedContract.ProofType.REGULAR_TRANSFER: {
const hashAlgorithm = /** @type {Hash.Algorithm} */ buf.readUint8();
const hashDepth = buf.readUint8();
const hashRoot = Hash.unserialize(buf, hashAlgorithm);
const preImage = Hash.unserialize(buf, hashAlgorithm);
const signatureProof = SignatureProof.unserialize(buf);
return {
type: HashedTimeLockedContract.ProofType.toString(type),
hashAlgorithm: Hash.Algorithm.toString(hashAlgorithm),
hashDepth,
hashRoot: hashRoot.toPlain(),
preImage: preImage.toPlain(),
signer: signatureProof.publicKey.toAddress().toPlain(),
signature: signatureProof.signature.toHex(),
publicKey: signatureProof.publicKey.toHex(),
pathLength: signatureProof.merklePath.nodes.length
};
}
case HashedTimeLockedContract.ProofType.EARLY_RESOLVE: {
const signatureProof = SignatureProof.unserialize(buf);
const creatorSignatureProof = SignatureProof.unserialize(buf);
return {
type: HashedTimeLockedContract.ProofType.toString(type),
signer: signatureProof.publicKey.toAddress().toPlain(),
signature: signatureProof.signature.toHex(),
publicKey: signatureProof.publicKey.toHex(),
pathLength: signatureProof.merklePath.nodes.length,
creator: creatorSignatureProof.publicKey.toAddress().toPlain(),
creatorSignature: creatorSignatureProof.signature.toHex(),
creatorPublicKey: creatorSignatureProof.publicKey.toHex(),
creatorPathLength: creatorSignatureProof.merklePath.nodes.length
};
}
case HashedTimeLockedContract.ProofType.TIMEOUT_RESOLVE: {
const creatorSignatureProof = SignatureProof.unserialize(buf);
return {
type: HashedTimeLockedContract.ProofType.toString(type),
creator: creatorSignatureProof.publicKey.toAddress().toPlain(),
creatorSignature: creatorSignatureProof.signature.toHex(),
creatorPublicKey: creatorSignatureProof.publicKey.toHex(),
creatorPathLength: creatorSignatureProof.merklePath.nodes.length
};
}
default:
return false;
}
} catch (e) {
return Account.proofToPlain(proof);
}
}
}
HashedTimeLockedContract.ProofType = {
REGULAR_TRANSFER: 1,
EARLY_RESOLVE: 2,
TIMEOUT_RESOLVE: 3
};
/**
* @param {HashedTimeLockedContract.ProofType} proofType
* @return {string}
*/
HashedTimeLockedContract.ProofType.toString = function(proofType) {
switch (proofType) {
case HashedTimeLockedContract.ProofType.REGULAR_TRANSFER: return 'regular-transfer';
case HashedTimeLockedContract.ProofType.EARLY_RESOLVE: return 'early-resolve';
case HashedTimeLockedContract.ProofType.TIMEOUT_RESOLVE: return 'timeout-resolve';
}
throw new Error('Invalid proof type');
};
Account.TYPE_MAP.set(Account.Type.HTLC, HashedTimeLockedContract);
Class.register(HashedTimeLockedContract);