Skip to content

Commit

Permalink
Add deposit aggreator
Browse files Browse the repository at this point in the history
  • Loading branch information
gitzhou committed Jan 9, 2025
1 parent 7ee0bcf commit ac58e76
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 0 deletions.
1 change: 1 addition & 0 deletions l1/.prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"semi": true
}
}
Expand Down
38 changes: 38 additions & 0 deletions l1/src/contracts/aggregatorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { assert, ByteString, hash256, len, method, sha256, Sha256, SmartContractLib, toByteString } from 'scrypt-ts';
import { AggregatorTx } from './types';
import { GeneralUtils } from './generalUtils';

export class AggregatorUtils extends SmartContractLib {
@method()
static getTxId(tx: AggregatorTx, isLeaf: boolean): Sha256 {
// leaf has no contract input, otherwise, contract input is a segwit input
const contractInputLen: bigint = isLeaf ? 0n : 37n;

assert(len(tx.version) == 4n);
assert(len(tx.contractInput0) == contractInputLen);
assert(len(tx.contractInput1) == contractInputLen);
assert(len(tx.feeInput) >= 37n);
assert(tx.contractOutputAmount > 0n);
assert(len(tx.contractOutputLocking) == 35n);
assert(len(tx.dataHash) == 32n);
assert(len(tx.locktime) == 4n);

const inputs = isLeaf
? toByteString('01') + tx.feeInput
: toByteString('03') + tx.contractInput0 + tx.contractInput1 + tx.feeInput;

return hash256(
tx.version +
inputs +
toByteString('02') +
GeneralUtils.buildStateOutput(tx.dataHash) +
GeneralUtils.buildContractOutput(tx.contractOutputAmount, tx.contractOutputLocking) +
tx.locktime,
);
}

@method()
static getHashPrevouts(prevTxId0: Sha256, prevTxId1: Sha256, feePrevout: ByteString): Sha256 {
return sha256(prevTxId0 + toByteString('00000000') + prevTxId1 + toByteString('00000000') + feePrevout);
}
}
146 changes: 146 additions & 0 deletions l1/src/contracts/depositAggregator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import {
assert,
ByteString,
hash256,
method,
prop,
PubKey,
sha256,
Sha256,
Sig,
SmartContract,
toByteString,
} from 'scrypt-ts';
import { AggregatorTx, DepositData, SHPreimage } from './types';
import { AggregatorUtils } from './aggregatorUtils';
import { SigHashUtils } from './sigHashUtils';
import { GeneralUtils } from './generalUtils';

export class DepositAggregator extends SmartContract {
@prop()
operatorPubKey: PubKey;

@prop()
bridgeLocking: ByteString;

constructor(operatorPubKey: PubKey, bridgeLocking: ByteString) {
super(...arguments);
this.operatorPubKey = operatorPubKey;
this.bridgeLocking = bridgeLocking;
}

/**
* Aggregates two aggregator transactions (or leaves) into one.
*
* @param shPreimage Sighash preimage of the currently executing transaction.
* @param sigOperator Signature of the bridge operator.
* @param isFirstInput Indicates whether this method is called from the first or second input.
* @param feePrevouts The prevout for the fee UTXO.
* @param prevTx0 Transaction data of the first previous transaction being aggregated. Can be a leaf transaction containing the deposit request itself or an already aggregated transaction.
* @param prevTx1 Transaction data of the second previous transaction being aggregated.
* @param isPrevTxLeaf Indicates whether the previous transactions are leaves.
* @param depositData0 Actual deposit data of the first deposit; used when aggregating leaves.
* @param depositData1 Actual deposit data of the second deposit; used when aggregating leaves.
* @param ancestorTx0 First ancestor transaction. These are used to inductively verify the transaction history; ignored when aggregating leaves.
* @param ancestorTx1 Second ancestor transaction.
* @param ancestorTx2 Third ancestor transaction.
* @param ancestorTx3 Fourth ancestor transaction.
* @param isAncestorLeaf Indicates whether the ancestor transactions are leaves.
*/
@method()
public aggregate(
shPreimage: SHPreimage,

sigOperator: Sig,
isFirstInput: boolean,
feePrevouts: ByteString,

prevTx0: AggregatorTx,
prevTx1: AggregatorTx,
isPrevTxLeaf: boolean,
depositData0: DepositData,
depositData1: DepositData,

ancestorTx0: AggregatorTx,
ancestorTx1: AggregatorTx,
ancestorTx2: AggregatorTx,
ancestorTx3: AggregatorTx,
isAncestorLeaf: boolean,
) {
// check sighash preimage
const s = SigHashUtils.checkSHPreimage(shPreimage);
assert(this.checkSig(s, SigHashUtils.Gx));

// check operator sig
assert(this.checkSig(sigOperator, this.operatorPubKey));

// check unlock inputIndex, this contract can only be unlocked at input #0 or #1
if (isFirstInput) {
assert(shPreimage.inputNumber == toByteString('00000000'));
} else {
assert(shPreimage.inputNumber == toByteString('01000000'));
}

// check prevouts if the passed prev txns are actually unlocked by the currently executing tx
const prevTxId0 = AggregatorUtils.getTxId(prevTx0, isPrevTxLeaf);
const prevTxId1 = AggregatorUtils.getTxId(prevTx1, isPrevTxLeaf);
const hashPrevouts = AggregatorUtils.getHashPrevouts(prevTxId0, prevTxId1, feePrevouts);
assert(shPreimage.hashPrevouts == hashPrevouts);

// check the two contract inputs are the same
assert(prevTx0.contractOutputLocking == prevTx1.contractOutputLocking);

if (isPrevTxLeaf) {
// if prev txns are leaves, check that the hash in their state output
// corresponds to the data passed in as witnesses
assert(prevTx0.dataHash == DepositAggregator.hashDepositData(depositData0));
assert(prevTx1.dataHash == DepositAggregator.hashDepositData(depositData1));

assert(prevTx0.contractOutputAmount == depositData0.amount);
assert(prevTx1.contractOutputAmount == depositData1.amount);
} else {
// if we're higher up the aggregation tree, we need to check the ancestor
// transactions to inductively validate the whole tree
const ancestorTxId0 = AggregatorUtils.getTxId(ancestorTx0, isAncestorLeaf);
const ancestorTxId1 = AggregatorUtils.getTxId(ancestorTx1, isAncestorLeaf);
const ancestorTxId2 = AggregatorUtils.getTxId(ancestorTx2, isAncestorLeaf);
const ancestorTxId3 = AggregatorUtils.getTxId(ancestorTx3, isAncestorLeaf);

// check prevTx0 unlocks ancestorTx0 and ancestorTx1
assert(prevTx0.contractInput0 == GeneralUtils.buildContractInput(ancestorTxId0));
assert(prevTx0.contractInput1 == GeneralUtils.buildContractInput(ancestorTxId1));

// check prevTx1 unlocks ancestorTx2 and ancestorTx3
assert(prevTx1.contractInput0 == GeneralUtils.buildContractInput(ancestorTxId2));
assert(prevTx1.contractInput1 == GeneralUtils.buildContractInput(ancestorTxId3));

// check ancestors have same contract locking as prev txns
// this completes the inductive step, since the successfull evaluation
// of the ancestors contract locking also checked its ancestors
assert(prevTx0.contractOutputLocking == ancestorTx0.contractOutputLocking);
assert(prevTx0.contractOutputLocking == ancestorTx1.contractOutputLocking);
assert(prevTx1.contractOutputLocking == ancestorTx2.contractOutputLocking);
assert(prevTx1.contractOutputLocking == ancestorTx3.contractOutputLocking);
}

// confine outputs
const dataHash = hash256(prevTx0.dataHash + prevTx1.dataHash);
const stateOutput = GeneralUtils.buildStateOutput(dataHash);

const contractOutputAmount = prevTx0.contractOutputAmount + prevTx1.contractOutputAmount;
const contractOutput = GeneralUtils.buildContractOutput(contractOutputAmount, prevTx0.contractOutputLocking);

const hashOutputs = sha256(stateOutput + contractOutput);
assert(shPreimage.hashOutputs == hashOutputs);
}

@method()
public finalize() {
assert(true);
}

@method()
static hashDepositData(depositData: DepositData): Sha256 {
return hash256(depositData.address + GeneralUtils.int32ToSatoshiBytes(depositData.amount));
}
}
63 changes: 63 additions & 0 deletions l1/src/contracts/generalUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
SmartContractLib,
method,
ByteString,
int2ByteString,
toByteString,
assert,
Sha256,
len,
OpCode,
} from 'scrypt-ts';
import { int32 } from './types';

export class GeneralUtils extends SmartContractLib {
@method()
static int32ToSatoshiBytes(amount: int32): ByteString {
assert(amount >= 0n);
let amountBytes = int2ByteString(amount);
const amountBytesLen = len(amountBytes);
if (amountBytesLen == 0n) {
amountBytes = toByteString('0000000000000000');
} else if (amountBytesLen == 1n) {
amountBytes += toByteString('00000000000000');
} else if (amountBytesLen == 2n) {
amountBytes += toByteString('000000000000');
} else if (amountBytesLen == 3n) {
amountBytes += toByteString('0000000000');
} else if (amountBytesLen == 4n) {
amountBytes += toByteString('00000000');
} else if (amountBytesLen == 5n) {
amountBytes += toByteString('000000');
} else if (amountBytesLen == 6n) {
amountBytes += toByteString('0000');
} else if (amountBytesLen == 7n) {
amountBytes += toByteString('00');
} else if (amountBytesLen > 8n) {
assert(false);
}
return amountBytes;
}

@method()
static buildStateOutput(hash: Sha256): ByteString {
return (
toByteString('0000000000000000') + // output satoshis
toByteString('22') + // script lenght (34 bytes)
OpCode.OP_RETURN +
toByteString('20') + // data hash length (32 bytes)
hash
);
}

@method()
static buildContractOutput(amount: int32, locking: ByteString): ByteString {
return GeneralUtils.int32ToSatoshiBytes(amount) + locking;
}

@method()
static buildContractInput(prevTxId: ByteString): ByteString {
// prevTxId + outputIndex (00000000) + unlockingScriptLen (00) + nSequence (ffffffff)
return prevTxId + toByteString('0000000000ffffffff');
}
}
57 changes: 57 additions & 0 deletions l1/src/contracts/sigHashUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
ByteString,
PubKey,
Sig,
SmartContractLib,
assert,
int2ByteString,
method,
prop,
sha256,
toByteString,
} from 'scrypt-ts';
import { SHPreimage } from './types';

export class SigHashUtils extends SmartContractLib {
// Data for checking sighash preimage:
@prop()
static readonly Gx: PubKey = PubKey(
toByteString('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
);
@prop()
static readonly ePreimagePrefix: ByteString = toByteString(
'7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179879be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
); // TAG_HASH + TAG_HASH + Gx + Gx
@prop()
static readonly preimagePrefix: ByteString = toByteString(
'f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a031f40a48df4b2a70c8b4924bf2654661ed3d95fd66a313eb87237597c628e4a0310000',
); // TAPSIGHASH + TAPSIGHASH + PREIMAGE_SIGHASH + PREIMAGE_EPOCH

@method()
static checkSHPreimage(shPreimage: SHPreimage): Sig {
assert(shPreimage.eSuffix > -127n && shPreimage.eSuffix < 127n, 'e suffix not in range [-126, 127)');
const e = sha256(SigHashUtils.ePreimagePrefix + shPreimage.sigHash);
assert(e == shPreimage._e + int2ByteString(shPreimage.eSuffix), 'invalid value of _e');
const sDelta: bigint = shPreimage.eSuffix < 0n ? -1n : 1n;
const s = SigHashUtils.Gx + shPreimage._e + int2ByteString(shPreimage.eSuffix + sDelta);
const sigHash = sha256(
SigHashUtils.preimagePrefix +
shPreimage.txVer +
shPreimage.nLockTime +
shPreimage.hashPrevouts +
shPreimage.hashSpentAmounts +
shPreimage.hashSpentScripts +
shPreimage.hashSequences +
shPreimage.hashOutputs +
shPreimage.spendType +
shPreimage.inputNumber +
shPreimage.hashTapLeaf +
shPreimage.keyVer +
shPreimage.codeSeparator,
);
assert(sigHash == shPreimage.sigHash, 'sigHash mismatch');

// assert(this.checkSig(Sig(s), SigHashUtils.Gx)) TODO (currently done outside)
return Sig(s);
}
}
38 changes: 38 additions & 0 deletions l1/src/contracts/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Addr, ByteString, Sha256 } from 'scrypt-ts';

export type int32 = bigint;

export type SHPreimage = {
txVer: ByteString;
nLockTime: ByteString;
hashPrevouts: ByteString;
hashSpentAmounts: ByteString;
hashSpentScripts: ByteString;
hashSequences: ByteString;
hashOutputs: ByteString;
spendType: ByteString;
inputNumber: ByteString;
hashTapLeaf: ByteString;
keyVer: ByteString;
codeSeparator: ByteString;

sigHash: ByteString;
_e: ByteString; // e without last byte
eSuffix: bigint; // last byte of e
};

export type AggregatorTx = {
version: ByteString;
contractInput0: ByteString;
contractInput1: ByteString;
feeInput: ByteString;
contractOutputAmount: int32;
contractOutputLocking: ByteString; // taproot output, lockingScriptSize (1 byte) + lockingScript (34 bytes)
dataHash: Sha256;
locktime: ByteString;
};

export type DepositData = {
address: Addr;
amount: int32;
};

0 comments on commit ac58e76

Please sign in to comment.