PHP WebShell

Текущая директория: /opt/BitGoJS/modules/babylonlabs-io-btc-staking-ts/src/staking

Просмотр файла: stakingScript.ts

import { opcodes, script } from "bitcoinjs-lib";
import { NO_COORD_PK_BYTE_LENGTH } from "../constants/keys";

export const MAGIC_BYTES_LEN = 4;

// Represents the staking scripts used in BTC staking.
export interface StakingScripts {
  timelockScript: Buffer;
  unbondingScript: Buffer;
  slashingScript: Buffer;
  unbondingTimelockScript: Buffer;
}

// StakingScriptData is a class that holds the data required for the BTC Staking Script
// and exposes methods for converting it into useful formats
export class StakingScriptData {
  stakerKey: Buffer;
  finalityProviderKeys: Buffer[];
  covenantKeys: Buffer[];
  covenantThreshold: number;
  stakingTimeLock: number;
  unbondingTimeLock: number;

  constructor(
    // The `stakerKey` is the public key of the staker without the coordinate bytes.
    stakerKey: Buffer,
    // A list of public keys without the coordinate bytes corresponding to the finality providers
    // the stake will be delegated to.
    // Currently, Babylon does not support restaking, so this should contain only a single item.
    finalityProviderKeys: Buffer[],
    // A list of the public keys without the coordinate bytes corresponding to
    // the covenant emulators.
    // This is a parameter of the Babylon system and should be retrieved from there.
    covenantKeys: Buffer[],
    // The number of covenant emulator signatures required for a transaction
    // to be valid.
    // This is a parameter of the Babylon system and should be retrieved from there.
    covenantThreshold: number,
    // The staking period denoted as a number of BTC blocks.
    stakingTimelock: number,
    // The unbonding period denoted as a number of BTC blocks.
    // This value should be more than equal than the minimum unbonding time of the
    // Babylon system.
    unbondingTimelock: number,
  ) {
    if (
      !stakerKey ||
      !finalityProviderKeys ||
      !covenantKeys ||
      !covenantThreshold ||
      !stakingTimelock ||
      !unbondingTimelock
    ) {
      throw new Error("Missing required input values");
    }
    this.stakerKey = stakerKey;
    this.finalityProviderKeys = finalityProviderKeys;
    this.covenantKeys = covenantKeys;
    this.covenantThreshold = covenantThreshold;
    this.stakingTimeLock = stakingTimelock;
    this.unbondingTimeLock = unbondingTimelock;

    // Run the validate method to check if the provided script data is valid
    if (!this.validate()) {
      throw new Error("Invalid script data provided");
    }
  }

  /**
   * Validates the staking script.
   * @returns {boolean} Returns true if the staking script is valid, otherwise false.
   */
  validate(): boolean {
    // check that staker key is the correct length
    if (this.stakerKey.length != NO_COORD_PK_BYTE_LENGTH) {
      return false;
    }
    // check that finalityProvider keys are the correct length
    if (
      this.finalityProviderKeys.some(
        (finalityProviderKey) => finalityProviderKey.length != NO_COORD_PK_BYTE_LENGTH,
      )
    ) {
      return false;
    }
    // check that covenant keys are the correct length
    if (
      this.covenantKeys.some((covenantKey) => covenantKey.length != NO_COORD_PK_BYTE_LENGTH)
    ) {
      return false;
    }

    // Check whether we have any duplicate keys
    const allPks = [
      this.stakerKey,
      ...this.finalityProviderKeys,
      ...this.covenantKeys,
    ];
    const allPksSet = new Set(allPks);
    if (allPks.length !== allPksSet.size) {
      return false;
    }

    // check that the threshold is above 0 and less than or equal to
    // the size of the covenant emulators set
    if (
      this.covenantThreshold <= 0 ||
      this.covenantThreshold > this.covenantKeys.length
    ) {
      return false;
    }

    // check that maximum value for staking time is not greater than uint16 and above 0
    if (this.stakingTimeLock <= 0 || this.stakingTimeLock > 65535) {
      return false;
    }

    // check that maximum value for unbonding time is not greater than uint16 and above 0
    if (this.unbondingTimeLock <= 0 || this.unbondingTimeLock > 65535) {
      return false;
    }

    return true;
  }

  // The staking script allows for multiple finality provider public keys
  // to support (re)stake to multiple finality providers
  // Covenant members are going to have multiple keys

  /**
   * Builds a timelock script.
   * @param timelock - The timelock value to encode in the script.
   * @returns {Buffer} containing the compiled timelock script.
   */
  buildTimelockScript(timelock: number): Buffer {
    return script.compile([
      this.stakerKey,
      opcodes.OP_CHECKSIGVERIFY,
      script.number.encode(timelock),
      opcodes.OP_CHECKSEQUENCEVERIFY,
    ]);
  }

  /**
   * Builds the staking timelock script.
   * Only holder of private key for given pubKey can spend after relative lock time
   * Creates the timelock script in the form:
   *    <stakerPubKey>
   *    OP_CHECKSIGVERIFY
   *    <stakingTimeBlocks>
   *    OP_CHECKSEQUENCEVERIFY
   * @returns {Buffer} The staking timelock script.
   */
  buildStakingTimelockScript(): Buffer {
    return this.buildTimelockScript(this.stakingTimeLock);
  }

  /**
   * Builds the unbonding timelock script.
   * Creates the unbonding timelock script in the form:
   *    <stakerPubKey>
   *    OP_CHECKSIGVERIFY
   *    <unbondingTimeBlocks>
   *    OP_CHECKSEQUENCEVERIFY
   * @returns {Buffer} The unbonding timelock script.
   */
  buildUnbondingTimelockScript(): Buffer {
    return this.buildTimelockScript(this.unbondingTimeLock);
  }

  /**
   * Builds the unbonding script in the form:
   *    buildSingleKeyScript(stakerPk, true) ||
   *    buildMultiKeyScript(covenantPks, covenantThreshold, false)
   *    || means combining the scripts
   * @returns {Buffer} The unbonding script.
   */
  buildUnbondingScript(): Buffer {
    return Buffer.concat([
      this.buildSingleKeyScript(this.stakerKey, true),
      this.buildMultiKeyScript(
        this.covenantKeys,
        this.covenantThreshold,
        false,
      ),
    ]);
  }

  /**
   * Builds the slashing script for staking in the form:
   *    buildSingleKeyScript(stakerPk, true) ||
   *    buildMultiKeyScript(finalityProviderPKs, 1, true) ||
   *    buildMultiKeyScript(covenantPks, covenantThreshold, false)
   *    || means combining the scripts
   * The slashing script is a combination of single-key and multi-key scripts.
   * The single-key script is used for staker key verification.
   * The multi-key script is used for finality provider key verification and covenant key verification.
   * @returns {Buffer} The slashing script as a Buffer.
   */
  buildSlashingScript(): Buffer {
    return Buffer.concat([
      this.buildSingleKeyScript(this.stakerKey, true),
      this.buildMultiKeyScript(
        this.finalityProviderKeys,
        // The threshold is always 1 as we only need one
        // finalityProvider signature to perform slashing
        // (only one finalityProvider performs an offence)
        1,
        // OP_VERIFY/OP_CHECKSIGVERIFY is added at the end
        true,
      ),
      this.buildMultiKeyScript(
        this.covenantKeys,
        this.covenantThreshold,
        // No need to add verify since covenants are at the end of the script
        false,
      ),
    ]);
  }
  /**
   * Builds the staking scripts.
   * @returns {StakingScripts} The staking scripts.
   */
  buildScripts(): StakingScripts {
    return {
      timelockScript: this.buildStakingTimelockScript(),
      unbondingScript: this.buildUnbondingScript(),
      slashingScript: this.buildSlashingScript(),
      unbondingTimelockScript: this.buildUnbondingTimelockScript(),
    };
  }

  // buildSingleKeyScript and buildMultiKeyScript allow us to reuse functionality
  // for creating Bitcoin scripts for the unbonding script and the slashing script

  /**
   * Builds a single key script in the form:
   * buildSingleKeyScript creates a single key script
   *    <pk> OP_CHECKSIGVERIFY (if withVerify is true)
   *    <pk> OP_CHECKSIG (if withVerify is false)
   * @param pk - The public key buffer.
   * @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode.
   * @returns The compiled script buffer.
   */
  buildSingleKeyScript(pk: Buffer, withVerify: boolean): Buffer {
    // Check public key length
    if (pk.length != NO_COORD_PK_BYTE_LENGTH) {
      throw new Error("Invalid key length");
    }
    return script.compile([
      pk,
      withVerify ? opcodes.OP_CHECKSIGVERIFY : opcodes.OP_CHECKSIG,
    ]);
  }

  /**
   * Builds a multi-key script in the form:
   *    <pk1> OP_CHEKCSIG <pk2> OP_CHECKSIGADD <pk3> OP_CHECKSIGADD ... <pkN> OP_CHECKSIGADD <threshold> OP_NUMEQUAL
   *    <withVerify -> OP_NUMEQUALVERIFY>
   * It validates whether provided keys are unique and the threshold is not greater than number of keys
   * If there is only one key provided it will return single key sig script
   * @param pks - An array of public keys.
   * @param threshold - The required number of valid signers.
   * @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode.
   * @returns The compiled multi-key script as a Buffer.
   * @throws {Error} If no keys are provided, if the required number of valid signers is greater than the number of provided keys, or if duplicate keys are provided.
   */
  buildMultiKeyScript(
    pks: Buffer[],
    threshold: number,
    withVerify: boolean,
  ): Buffer {
    // Verify that pks is not empty
    if (!pks || pks.length === 0) {
      throw new Error("No keys provided");
    }
    // Check buffer object have expected lengths like checking pks.length
    if (pks.some((pk) => pk.length != NO_COORD_PK_BYTE_LENGTH)) {
      throw new Error("Invalid key length");
    }
    // Verify that threshold <= len(pks)
    if (threshold > pks.length) {
      throw new Error(
        "Required number of valid signers is greater than number of provided keys",
      );
    }
    if (pks.length === 1) {
      return this.buildSingleKeyScript(pks[0], withVerify);
    }
    // keys must be sorted
    const sortedPks = [...pks].sort(Buffer.compare);
    // verify there are no duplicates
    for (let i = 0; i < sortedPks.length - 1; ++i) {
      if (sortedPks[i].equals(sortedPks[i + 1])) {
        throw new Error("Duplicate keys provided");
      }
    }
    const scriptElements = [sortedPks[0], opcodes.OP_CHECKSIG];
    for (let i = 1; i < sortedPks.length; i++) {
      scriptElements.push(sortedPks[i]);
      scriptElements.push(opcodes.OP_CHECKSIGADD);
    }
    scriptElements.push(script.number.encode(threshold));
    if (withVerify) {
      scriptElements.push(opcodes.OP_NUMEQUALVERIFY);
    } else {
      scriptElements.push(opcodes.OP_NUMEQUAL);
    }
    return script.compile(scriptElements);
  }
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!