test/specs/generic/consensus/base/account/HashedTimeLockedContract.spec.js
describe('HashedTimeLockedContract', () => {
const recipient = Address.unserialize(BufferUtils.fromBase64(Dummy.address1));
const sender = Address.unserialize(BufferUtils.fromBase64(Dummy.address2));
it('can serialize and unserialize itself', () => {
const account = new HashedTimeLockedContract(1000, sender, recipient, Hash.NULL, 42, 1000, 800);
const account2 = /** @type {HashedTimeLockedContract} */ Account.unserialize(account.serialize());
expect(account2 instanceof HashedTimeLockedContract).toBeTruthy();
expect(account2.type).toEqual(account.type);
expect(account2.balance).toEqual(account.balance);
expect(account2.recipient.equals(account.recipient)).toBeTruthy();
expect(account2.sender.equals(account.sender)).toBeTruthy();
expect(account2.hashRoot.equals(account.hashRoot)).toBeTruthy();
expect(account2.hashCount).toEqual(account.hashCount);
expect(account2.totalAmount).toEqual(account.totalAmount);
expect(account2.timeout).toEqual(account.timeout);
});
it('will deny incoming transaction after creation', () => {
const account = new HashedTimeLockedContract();
let transaction = new ExtendedTransaction(Address.NULL, Account.Type.BASIC, Address.NULL, Account.Type.BASIC, 1000, 0, 1, Transaction.Flag.NONE, new Uint8Array(0));
expect(() => account.withIncomingTransaction(transaction, 1)).toThrowError();
transaction = new ExtendedTransaction(Address.NULL, Account.Type.BASIC, Address.NULL, Account.Type.BASIC, 1000, 0, 1, Transaction.Flag.CONTRACT_CREATION, new Uint8Array(0));
expect(() => account.withIncomingTransaction(transaction, 1)).toThrowError();
});
it('can falsify invalid incoming transaction', (done) => {
(async () => {
// No data
let transaction = new ExtendedTransaction(sender, Account.Type.BASIC, Address.NULL, Account.Type.HTLC, 100, 0, 0, Transaction.Flag.NONE, new Uint8Array(0));
expect(await HashedTimeLockedContract.verifyIncomingTransaction(transaction)).toBeFalsy();
// Data too short
transaction = new ExtendedTransaction(sender, Account.Type.BASIC, Address.NULL, Account.Type.HTLC, 100, 0, 0, Transaction.Flag.NONE, new Uint8Array(18));
expect(await HashedTimeLockedContract.verifyIncomingTransaction(transaction)).toBeFalsy();
// Data too long
let data = new SerialBuffer(Address.SERIALIZED_SIZE * 2 + Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + 6 + 4711);
sender.serialize(data);
recipient.serialize(data);
data.writeUint8(Hash.Algorithm.BLAKE2B);
Hash.NULL.serialize(data);
data.writeUint8(2);
data.writeUint32(1000);
transaction = new ExtendedTransaction(sender, Account.Type.BASIC, Address.CONTRACT_CREATION, Account.Type.HTLC, 100, 0, 0, Transaction.Flag.NONE, data);
expect(await HashedTimeLockedContract.verifyIncomingTransaction(transaction)).toBeFalsy();
// Invalid hash type
data = new SerialBuffer(Address.SERIALIZED_SIZE * 2 + Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + 6);
sender.serialize(data);
recipient.serialize(data);
data.writeUint8(251);
Hash.NULL.serialize(data);
data.writeUint8(2);
data.writeUint32(1000);
transaction = new ExtendedTransaction(sender, Account.Type.BASIC, Address.CONTRACT_CREATION, Account.Type.HTLC, 100, 0, 0, Transaction.Flag.NONE, data);
expect(await HashedTimeLockedContract.verifyIncomingTransaction(transaction)).toBeFalsy();
// Invalid contract address
data = new SerialBuffer(Address.SERIALIZED_SIZE * 2 + Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + 6);
sender.serialize(data);
recipient.serialize(data);
data.writeUint8(Hash.Algorithm.BLAKE2B);
Hash.NULL.serialize(data);
data.writeUint8(2);
data.writeUint32(1000);
transaction = new ExtendedTransaction(sender, Account.Type.BASIC, Address.NULL, Account.Type.HTLC, 100, 0, 0, Transaction.Flag.NONE, data);
expect(await HashedTimeLockedContract.verifyIncomingTransaction(transaction)).toBeFalsy();
// Sanity check
transaction = new ExtendedTransaction(sender, Account.Type.BASIC, Address.CONTRACT_CREATION, Account.Type.HTLC, 100, 0, 0, Transaction.Flag.NONE, data);
expect(await HashedTimeLockedContract.verifyIncomingTransaction(transaction)).toBeTruthy();
})().then(done, done.fail);
});
it('can falsify invalid outgoing transaction', (done) => {
(async () => {
const keyPair = KeyPair.generate();
const addr = keyPair.publicKey.toAddress();
// No proof
let transaction = new ExtendedTransaction(sender, Account.Type.HTLC, addr, Account.Type.BASIC, 100, 0, 1, Transaction.Flag.NONE, new Uint8Array(0));
let signatureProof = SignatureProof.singleSig(keyPair.publicKey, Signature.create(keyPair.privateKey, keyPair.publicKey, transaction.serializeContent()));
let brokenSignatureProof = SignatureProof.singleSig(keyPair.publicKey, new Signature(new Uint8Array(64)));
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// Proof too short
let proof = new SerialBuffer(1);
proof.writeUint8(HashedTimeLockedContract.ProofType.REGULAR_TRANSFER);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// Proof too long
proof = new SerialBuffer(3 + 2 * Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + signatureProof.serializedSize + 4711);
proof.writeUint8(HashedTimeLockedContract.ProofType.REGULAR_TRANSFER);
proof.writeUint8(Hash.Algorithm.BLAKE2B);
proof.writeUint8(1);
Hash.blake2b(Hash.NULL.array).serialize(proof);
Hash.NULL.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// invalid proof type
proof = new SerialBuffer(1);
proof.writeUint8(200);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// regular: invalid pre-image
proof = new SerialBuffer(3 + 2 * Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.REGULAR_TRANSFER);
proof.writeUint8(Hash.Algorithm.BLAKE2B);
proof.writeUint8(1);
Hash.NULL.serialize(proof);
Hash.NULL.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// regular: invalid signature
proof = new SerialBuffer(3 + 2 * Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.REGULAR_TRANSFER);
proof.writeUint8(Hash.Algorithm.BLAKE2B);
proof.writeUint8(1);
Hash.blake2b(Hash.NULL.serialize()).serialize(proof);
Hash.NULL.serialize(proof);
brokenSignatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// regular: invalid hash type
proof = new SerialBuffer(3 + 2 * Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.REGULAR_TRANSFER);
proof.writeUint8(251);
proof.writeUint8(0);
Hash.NULL.serialize(proof);
Hash.NULL.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// early resolve: invalid second signature
proof = new SerialBuffer(1 + 2 * signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.EARLY_RESOLVE);
signatureProof.serialize(proof);
brokenSignatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// early resolve: invalid first signature
proof = new SerialBuffer(1 + 2 * signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.EARLY_RESOLVE);
brokenSignatureProof.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// timeout resolve: invalid signature
proof = new SerialBuffer(1 + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.TIMEOUT_RESOLVE);
brokenSignatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
transaction = new ExtendedTransaction(sender, Account.Type.HTLC, recipient, Account.Type.BASIC, 100, 0, 0, Transaction.Flag.NONE, new Uint8Array(0));
signatureProof = SignatureProof.singleSig(keyPair.publicKey, Signature.create(keyPair.privateKey, keyPair.publicKey, transaction.serializeContent()));
/*
TODO: No longer fail, should create proper replacement tests
// timeout resolve: mismatch signature
proof = new SerialBuffer(1 + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.TIMEOUT_RESOLVE);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// early resolve: mismatch first signature
proof = new SerialBuffer(1 + 2 * signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.EARLY_RESOLVE);
signatureProof.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// early resolve: mismatch second signature
proof = new SerialBuffer(1 + Address.SERIALIZED_SIZE + 2 * signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.EARLY_RESOLVE);
addr.serialize(proof);
signatureProof.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
// regular: mismatch signature
proof = new SerialBuffer(3 + 2 * Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.REGULAR_TRANSFER);
proof.writeUint8(Hash.Algorithm.BLAKE2B);
proof.writeUint8(1);
(await Hash.light(Hash.NULL.serialize())).serialize(proof);
Hash.NULL.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeFalsy();
*/
})().then(done, done.fail);
});
it('can verify valid outgoing transaction', (done) => {
(async () => {
const keyPair = KeyPair.generate();
const addr = keyPair.publicKey.toAddress();
const transaction = new ExtendedTransaction(sender, Account.Type.HTLC, addr, Account.Type.BASIC, 100, 0, 1, Transaction.Flag.NONE, new Uint8Array(0));
// regular
const signatureProof = new SignatureProof(keyPair.publicKey, new MerklePath([]), Signature.create(keyPair.privateKey, keyPair.publicKey, transaction.serializeContent()));
let proof = new SerialBuffer(3 + 2 * Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.REGULAR_TRANSFER);
proof.writeUint8(Hash.Algorithm.BLAKE2B);
proof.writeUint8(1);
Hash.blake2b(Hash.NULL.array).serialize(proof);
Hash.NULL.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeTruthy();
// early resolve
proof = new SerialBuffer(1 + 2 * signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.EARLY_RESOLVE);
signatureProof.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeTruthy();
// timeout resolve
proof = new SerialBuffer(1 + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.TIMEOUT_RESOLVE);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeTruthy();
})().then(done, done.fail);
});
it('can redeem funds regularly before timeout', (done) => {
(async () => {
const cache = new TransactionCache();
const keyPair = KeyPair.generate();
const addr = keyPair.publicKey.toAddress();
const hashRoot = Hash.blake2b(Hash.NULL.array);
const transaction = new ExtendedTransaction(recipient, Account.Type.HTLC, addr, Account.Type.BASIC, 100, 0, 1, Transaction.Flag.NONE, new Uint8Array(0));
const account = new HashedTimeLockedContract(1000, sender, addr, hashRoot, 1, 1000, 1000);
const signatureProof = new SignatureProof(keyPair.publicKey, new MerklePath([]), Signature.create(keyPair.privateKey, keyPair.publicKey, transaction.serializeContent()));
const proof = new SerialBuffer(3 + 2 * Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.REGULAR_TRANSFER);
proof.writeUint8(Hash.Algorithm.BLAKE2B);
proof.writeUint8(1);
hashRoot.serialize(proof);
Hash.NULL.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeTruthy();
expect(() => account.withOutgoingTransaction(transaction, 2000, cache)).toThrowError();
const account2 = account.withOutgoingTransaction(transaction, 100, cache);
expect(account2.balance).toBe(900);
})().then(done, done.fail);
});
it('can use sha256', (done) => {
(async () => {
const cache = new TransactionCache();
const keyPair = KeyPair.generate();
const addr = keyPair.publicKey.toAddress();
const hashRoot = await Hash.sha256(Hash.NULL.array);
const transaction = new ExtendedTransaction(recipient, Account.Type.HTLC, addr, Account.Type.BASIC, 100, 0, 1, Transaction.Flag.NONE, new Uint8Array(0));
const account = new HashedTimeLockedContract(1000, sender, addr, hashRoot, 1, 1000, 1000);
const signatureProof = new SignatureProof(keyPair.publicKey, new MerklePath([]), Signature.create(keyPair.privateKey, keyPair.publicKey, transaction.serializeContent()));
const proof = new SerialBuffer(3 + 2 * Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.REGULAR_TRANSFER);
proof.writeUint8(Hash.Algorithm.SHA256);
proof.writeUint8(1);
hashRoot.serialize(proof);
Hash.NULL.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeTruthy();
expect(() => account.withOutgoingTransaction(transaction, 2000, cache)).toThrowError();
const account2 = account.withOutgoingTransaction(transaction, 100, cache);
expect(account2.balance).toBe(900);
})().then(done, done.fail);
});
it('blocks regular redemption of too many funds', (done) => {
(async () => {
const cache = new TransactionCache();
const keyPair = KeyPair.generate();
const addr = keyPair.publicKey.toAddress();
const hashRoot = Hash.blake2b(Hash.NULL.array);
const transaction = new ExtendedTransaction(recipient, Account.Type.HTLC, addr, Account.Type.BASIC, 600, 0, 100, Transaction.Flag.NONE, new Uint8Array(0));
const account = new HashedTimeLockedContract(1000, sender, addr, hashRoot, 2, 1000, 1000);
const signatureProof = new SignatureProof(keyPair.publicKey, new MerklePath([]), Signature.create(keyPair.privateKey, keyPair.publicKey, transaction.serializeContent()));
const proof = new SerialBuffer(3 + 2 * Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.REGULAR_TRANSFER);
proof.writeUint8(Hash.Algorithm.BLAKE2B);
proof.writeUint8(1);
hashRoot.serialize(proof);
Hash.NULL.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeTruthy();
expect(() => account.withOutgoingTransaction(transaction, 100, cache)).toThrowError();
})().then(done, done.fail);
});
it('correctly allows to resolve after timeout', (done) => {
(async () => {
const cache = new TransactionCache();
const keyPair = KeyPair.generate();
const addr = keyPair.publicKey.toAddress();
const hashRoot = Hash.blake2b(Hash.NULL.array);
let transaction = new ExtendedTransaction(sender, Account.Type.HTLC, addr, Account.Type.BASIC, 100, 0, 500, Transaction.Flag.NONE, new Uint8Array(0));
const account = new HashedTimeLockedContract(1000, addr, recipient, hashRoot, 1, 1000, 1000);
let signatureProof = new SignatureProof(keyPair.publicKey, new MerklePath([]), Signature.create(keyPair.privateKey, keyPair.publicKey, transaction.serializeContent()));
let proof = new SerialBuffer(1 + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.TIMEOUT_RESOLVE);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeTruthy();
expect(() => account.withOutgoingTransaction(transaction, 500, cache)).toThrowError();
transaction = new ExtendedTransaction(sender, Account.Type.HTLC, addr, Account.Type.BASIC, 100, 0, 2000, Transaction.Flag.NONE, new Uint8Array(0));
signatureProof = new SignatureProof(keyPair.publicKey, new MerklePath([]), Signature.create(keyPair.privateKey, keyPair.publicKey, transaction.serializeContent()));
proof = new SerialBuffer(1 + signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.TIMEOUT_RESOLVE);
signatureProof.serialize(proof);
transaction.proof = proof;
const account2 = account.withOutgoingTransaction(transaction, 2000, cache);
expect(account2.balance).toBe(900);
})().then(done, done.fail);
});
it('correctly allows to resolve before timeout', (done) => {
(async () => {
const cache = new TransactionCache();
const keyPair = KeyPair.generate();
const addr = keyPair.publicKey.toAddress();
const hashRoot = Hash.blake2b(Hash.NULL.array);
const transaction = new ExtendedTransaction(sender, Account.Type.HTLC, addr, Account.Type.BASIC, 100, 0, 1, Transaction.Flag.NONE, new Uint8Array(0));
const account = new HashedTimeLockedContract(1000, addr, addr, hashRoot, 1, 1000, 1000);
const signatureProof = new SignatureProof(keyPair.publicKey, new MerklePath([]), Signature.create(keyPair.privateKey, keyPair.publicKey, transaction.serializeContent()));
const proof = new SerialBuffer(1 + 2 * signatureProof.serializedSize);
proof.writeUint8(HashedTimeLockedContract.ProofType.EARLY_RESOLVE);
signatureProof.serialize(proof);
signatureProof.serialize(proof);
transaction.proof = proof;
expect(await HashedTimeLockedContract.verifyOutgoingTransaction(transaction)).toBeTruthy();
const account2 = account.withOutgoingTransaction(transaction, 100, cache);
expect(account2.balance).toBe(900);
const account3 = new HashedTimeLockedContract(1000, addr, recipient, hashRoot, 1, 1000, 1000);
expect(() => account3.withOutgoingTransaction(transaction, 100, cache)).toThrowError();
})().then(done, done.fail);
});
it('can create contract from transaction', (done) => {
(async () => {
const data = new SerialBuffer(Address.SERIALIZED_SIZE * 2 + Hash.SIZE.get(Hash.Algorithm.BLAKE2B) + 6);
sender.serialize(data);
recipient.serialize(data);
data.writeUint8(Hash.Algorithm.BLAKE2B);
Hash.NULL.serialize(data);
data.writeUint8(2);
data.writeUint32(1000);
const transaction = new ExtendedTransaction(sender, Account.Type.BASIC, Address.CONTRACT_CREATION, Account.Type.HTLC, 100, 0, 0, Transaction.Flag.CONTRACT_CREATION, data);
expect(await HashedTimeLockedContract.verifyIncomingTransaction(transaction)).toBeTruthy();
const contract = /** @type {HashedTimeLockedContract} */ Account.INITIAL.withIncomingTransaction(transaction, 1).withContractCommand(transaction, 1);
expect(contract.balance).toBe(100);
expect(contract.sender.equals(sender)).toBeTruthy();
expect(contract.recipient.equals(recipient)).toBeTruthy();
expect(contract.hashRoot.equals(Hash.NULL)).toBeTruthy();
expect(contract.hashCount).toBe(2);
expect(contract.timeout).toBe(1000);
expect(contract.withContractCommand(transaction, 1, true).withIncomingTransaction(transaction, 1, true).isInitial()).toBeTruthy();
})().then(done, done.fail);
});
it('has toString method', (done) => {
(async function () {
const account = new HashedTimeLockedContract(100);
expect(() => account.toString()).not.toThrow();
})().then(done, done.fail);
});
});