All files / lib/messages OracleAttestation.ts

79.63% Statements 86/108
74.42% Branches 32/43
69.23% Functions 9/13
80% Lines 84/105

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 2931x 1x   1x                       1x 1x             14x 14x   14x 14x     14x   14x   14x     14x 12x 1x   11x 11x   12x     2x 2x 2x                 14x   14x 137x 137x         14x 14x   14x   14x         14x   14x           12x 101x 101x 101x       2x 2x 36x 36x 36x                           14x           20x                     20x     20x                   4x       4x         4x         4x         4x 38x               4x 38x           4x 38x 38x 38x 38x   1x             3x 1x                       1x         1x         1x 1x               1x 18x 18x   18x                                                                   8x 8x   8x 8x 8x 8x 8x   8x 49x       8x 8x 49x 49x     8x 8x   8x                      
import { BufferReader, BufferWriter } from '@node-dlc/bufio';
import { math, verify } from 'bip-schnorr';
 
import { MessageType } from '../MessageType';
import { IDlcMessage } from './DlcMessage';
import { OracleAnnouncement } from './OracleAnnouncement';
 
/**
 * Oracle attestation providing signatures over an outcome value.
 * This represents the oracle's actual attestation to a specific outcome.
 * Updated to match rust-dlc specification with 2-byte count prefixes.
 *
 * An attestation from an oracle providing signatures over an outcome value.
 * This is what the oracle publishes when they want to attest to a specific outcome.
 */
export class OracleAttestation implements IDlcMessage {
  public static type = MessageType.OracleAttestation;
 
  /**
   * Deserializes an oracle_attestation message
   * @param buf
   */
  public static deserialize(buf: Buffer): OracleAttestation {
    const instance = new OracleAttestation();
    const reader = new BufferReader(buf);
 
    reader.readBigSize(); // read type
    instance.length = reader.readBigSize();
 
    // Detect format: old rust-dlc 0.4.0 (no event_id) vs new rust-dlc (with event_id)
    const currentPos = reader.position;
 
    try {
      // Try reading as new format (with event_id)
      const eventIdLength = reader.readBigSize();
 
      // If event ID length is reasonable (0-100 bytes), assume new format
      if (eventIdLength >= BigInt('0') && eventIdLength <= BigInt('100')) {
        if (eventIdLength === BigInt('0')) {
          instance.eventId = '';
        } else {
          const eventIdBuf = reader.readBytes(Number(eventIdLength));
          instance.eventId = eventIdBuf.toString();
        }
        instance.oraclePubkey = reader.readBytes(32);
      } else {
        // Event ID length is unreasonable, probably old format without event_id
        reader.position = currentPos;
        instance.eventId = ''; // Default empty event ID for old format
        instance.oraclePubkey = reader.readBytes(32);
      }
    } catch (error) {
      // If reading fails, assume old format without event_id
      reader.position = currentPos;
      instance.eventId = ''; // Default empty event ID for old format
      instance.oraclePubkey = reader.readBytes(32);
    }
 
    const numSignatures = reader.readUInt16BE();
 
    for (let i = 0; i < numSignatures; i++) {
      const signature = reader.readBytes(64);
      instance.signatures.push(signature);
    }
 
    // Handle both rust-dlc format (with u16 count prefix) and DLCSpecs format (no count prefix)
    // Try to detect format by checking if next 2 bytes look like a reasonable outcome count
    Eif (!reader.eof) {
      const currentPos = reader.position;
 
      try {
        // Try reading as rust-dlc format (with u16 count prefix)
        const numOutcomes = reader.readUInt16BE();
 
        // Validate that this looks like a reasonable count
        // If it's > 1000 or the remaining bytes can't accommodate this many outcomes,
        // it's probably not a count prefix
        const remainingBytes = reader.buffer.length - reader.position;
 
        if (
          numOutcomes > 0 &&
          numOutcomes <= 1000 &&
          remainingBytes >= numOutcomes * 2
        ) {
          // Looks like rust-dlc format with u16 count prefix
          for (let i = 0; i < numOutcomes; i++) {
            const outcomeLen = reader.readBigSize();
            const outcomeBuf = reader.readBytes(Number(outcomeLen));
            instance.outcomes.push(outcomeBuf.toString());
          }
        } else {
          // Reset and try DLCSpecs format (no count prefix)
          reader.position = currentPos;
          while (!reader.eof) {
            const outcomeLen = reader.readBigSize();
            const outcomeBuf = reader.readBytes(Number(outcomeLen));
            instance.outcomes.push(outcomeBuf.toString());
          }
        }
      } catch (error) {
        // If reading as rust-dlc format fails, reset and try DLCSpecs format
        reader.position = currentPos;
        while (!reader.eof) {
          const outcomeLen = reader.readBigSize();
          const outcomeBuf = reader.readBytes(Number(outcomeLen));
          instance.outcomes.push(outcomeBuf.toString());
        }
      }
    }
 
    return instance;
  }
 
  /**
   * The type for oracle_attestation message. oracle_attestation = 55400
   */
  public type = OracleAttestation.type;
 
  public length: bigint;
 
  /** The identifier of the announcement. */
  public eventId: string;
 
  /** The public key of the oracle (32 bytes, x-only). */
  public oraclePubkey: Buffer;
 
  /** The signatures over the event outcome (64 bytes each, Schnorr format). */
  public signatures: Buffer[] = [];
 
  /** The set of strings representing the outcome value. */
  public outcomes: string[] = [];
 
  /**
   * Validates the oracle attestation according to rust-dlc specification.
   * This includes validating signatures and ensuring consistency with announcement.
   * @param announcement The corresponding oracle announcement for validation (optional)
   * @throws Will throw an error if validation fails
   */
  public validate(announcement?: OracleAnnouncement): void {
    // Basic structure validation
    Iif (this.signatures.length !== this.outcomes.length) {
      throw new Error('Number of signatures must match number of outcomes');
    }
 
    Iif (this.signatures.length === 0) {
      throw new Error('Must have at least one signature and outcome');
    }
 
    // Validate event ID
    Iif (!this.eventId || this.eventId.length === 0) {
      throw new Error('Event ID cannot be empty');
    }
 
    // Validate oracle public key format
    Iif (!this.oraclePubkey || this.oraclePubkey.length !== 32) {
      throw new Error('Oracle public key must be 32 bytes (x-only format)');
    }
 
    // Validate signature formats
    this.signatures.forEach((sig, index) => {
      Iif (!sig || sig.length !== 64) {
        throw new Error(
          `Signature at index ${index} must be 64 bytes (Schnorr format)`,
        );
      }
    });
 
    // Validate outcomes are not empty
    this.outcomes.forEach((outcome, index) => {
      Iif (!outcome || outcome.length === 0) {
        throw new Error(`Outcome at index ${index} cannot be empty`);
      }
    });
 
    // Verify signatures over outcomes using tagged hash
    this.signatures.forEach((sig, index) => {
      const outcome = this.outcomes[index];
      try {
        const msg = math.taggedHash('DLC/oracle/attestation/v0', outcome);
        verify(this.oraclePubkey, msg, sig);
      } catch (error) {
        throw new Error(
          `Invalid signature for outcome "${outcome}" at index ${index}: ${error.message}`,
        );
      }
    });
 
    // If announcement is provided, validate consistency
    if (announcement) {
      this.validateAgainstAnnouncement(announcement);
    }
  }
 
  /**
   * Validates the attestation against the corresponding oracle announcement.
   * This ensures the attestation is consistent with the original announcement.
   * @param announcement The oracle announcement to validate against
   * @throws Will throw an error if validation fails
   */
  private validateAgainstAnnouncement(announcement: OracleAnnouncement): void {
    // Validate oracle public key matches announcement
    Iif (!this.oraclePubkey.equals(announcement.oraclePubkey)) {
      throw new Error('Oracle public key must match announcement');
    }
 
    // Validate event ID matches
    Iif (this.eventId !== announcement.getEventId()) {
      throw new Error('Event ID must match announcement');
    }
 
    // Validate that the number of signatures matches the number of nonces in announcement
    const announcementNonces = announcement.getNonces();
    Iif (this.signatures.length !== announcementNonces.length) {
      throw new Error(
        'Number of signatures must match number of nonces in announcement',
      );
    }
 
    // Extract nonces from signatures (first 32 bytes) and compare with announcement nonces
    // This validates that the signatures were created using the committed nonces
    this.signatures.forEach((sig, index) => {
      const nonceFromSig = sig.slice(0, 32);
      const expectedNonce = announcementNonces[index];
 
      Iif (!nonceFromSig.equals(expectedNonce)) {
        throw new Error(
          `Signature nonce mismatch at index ${index}: signature was not created with announced nonce`,
        );
      }
    });
  }
 
  /**
   * Returns the nonces used by the oracle to sign the event outcome.
   * This is used for finding the matching oracle announcement.
   * The nonce is extracted from the first 32 bytes of each signature.
   */
  public getNonces(): Buffer[] {
    return this.signatures.map((sig) => sig.slice(0, 32));
  }
 
  /**
   * Converts oracle_attestation to JSON
   */
  public toJSON(): OracleAttestationJSON {
    return {
      type: this.type,
      eventId: this.eventId,
      oraclePubkey: this.oraclePubkey.toString('hex'),
      signatures: this.signatures.map((sig) => sig.toString('hex')),
      outcomes: this.outcomes,
    };
  }
 
  /**
   * Serializes the oracle_attestation message into a Buffer
   */
  public serialize(): Buffer {
    const writer = new BufferWriter();
    writer.writeBigSize(this.type);
 
    const dataWriter = new BufferWriter();
    dataWriter.writeBigSize(this.eventId.length);
    dataWriter.writeBytes(Buffer.from(this.eventId));
    dataWriter.writeBytes(this.oraclePubkey);
    dataWriter.writeUInt16BE(this.signatures.length);
 
    for (const signature of this.signatures) {
      dataWriter.writeBytes(signature);
    }
 
    // Write outcomes with u16 count prefix (matching rust-dlc format)
    dataWriter.writeUInt16BE(this.outcomes.length);
    for (const outcome of this.outcomes) {
      dataWriter.writeBigSize(outcome.length);
      dataWriter.writeBytes(Buffer.from(outcome));
    }
 
    writer.writeBigSize(dataWriter.size);
    writer.writeBytes(dataWriter.toBuffer());
 
    return writer.toBuffer();
  }
}
 
export interface OracleAttestationJSON {
  type: number;
  eventId: string;
  oraclePubkey: string;
  signatures: string[];
  outcomes: string[];
}