From ac58e760c28863dd777c57fb4cff9103a65cb0ae Mon Sep 17 00:00:00 2001 From: aaron67 Date: Fri, 10 Jan 2025 05:18:11 +0800 Subject: [PATCH] Add deposit aggreator --- l1/.prettierrc | 1 + l1/src/contracts/aggregatorUtils.ts | 38 +++++++ l1/src/contracts/depositAggregator.ts | 146 ++++++++++++++++++++++++++ l1/src/contracts/generalUtils.ts | 63 +++++++++++ l1/src/contracts/sigHashUtils.ts | 57 ++++++++++ l1/src/contracts/types.ts | 38 +++++++ 6 files changed, 343 insertions(+) create mode 100644 l1/src/contracts/aggregatorUtils.ts create mode 100644 l1/src/contracts/depositAggregator.ts create mode 100644 l1/src/contracts/generalUtils.ts create mode 100644 l1/src/contracts/sigHashUtils.ts create mode 100644 l1/src/contracts/types.ts diff --git a/l1/.prettierrc b/l1/.prettierrc index 4c24932..d3cd4d3 100644 --- a/l1/.prettierrc +++ b/l1/.prettierrc @@ -7,6 +7,7 @@ "tabWidth": 4, "singleQuote": true, "trailingComma": "all", + "printWidth": 120, "semi": true } } diff --git a/l1/src/contracts/aggregatorUtils.ts b/l1/src/contracts/aggregatorUtils.ts new file mode 100644 index 0000000..f1f152f --- /dev/null +++ b/l1/src/contracts/aggregatorUtils.ts @@ -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); + } +} diff --git a/l1/src/contracts/depositAggregator.ts b/l1/src/contracts/depositAggregator.ts new file mode 100644 index 0000000..2207e41 --- /dev/null +++ b/l1/src/contracts/depositAggregator.ts @@ -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)); + } +} diff --git a/l1/src/contracts/generalUtils.ts b/l1/src/contracts/generalUtils.ts new file mode 100644 index 0000000..49f5a0f --- /dev/null +++ b/l1/src/contracts/generalUtils.ts @@ -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'); + } +} diff --git a/l1/src/contracts/sigHashUtils.ts b/l1/src/contracts/sigHashUtils.ts new file mode 100644 index 0000000..68a09b3 --- /dev/null +++ b/l1/src/contracts/sigHashUtils.ts @@ -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); + } +} diff --git a/l1/src/contracts/types.ts b/l1/src/contracts/types.ts new file mode 100644 index 0000000..6f66051 --- /dev/null +++ b/l1/src/contracts/types.ts @@ -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; +};