-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
"tabWidth": 4, | ||
"singleQuote": true, | ||
"trailingComma": "all", | ||
"printWidth": 120, | ||
"semi": true | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |