Skip to content

Commit

Permalink
chore(offer): validate vin index on PSBTs
Browse files Browse the repository at this point in the history
  • Loading branch information
kodemon committed May 22, 2023
1 parent 0da020a commit 2fcb465
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 28 deletions.
12 changes: 11 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,15 @@
{
"mode": "auto"
}
],
"cSpell.words": [
"bitcoinjs",
"fastify",
"ipfs",
"psbt",
"sado",
"txid",
"utxo",
"vout"
]
}
}
35 changes: 27 additions & 8 deletions src/JsonRpc/Errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import { RpcError } from "./Core";

/**
* The **HTTP 400 Bad Request** response status code indicates that the server
* cannot or will not process the request due to something that is perceived to
* be a client error.
*/
export class BadRequestError<D = unknown> extends RpcError<D> {
/**
* Instantiate a new BadRequestError.
*
* @param data - Optional data to send with the error.
*/
constructor(message = "Bad Request", data?: D) {
super(message, -32000, data);
}
}

/**
* The **HTTP 401 Unauthorized** response status code indicates that the client
* request has not been completed because it lacks valid authentication
Expand All @@ -21,8 +37,8 @@ export class UnauthorizedError<D = unknown> extends RpcError<D> {
*
* @param data - Optional data to send with the error.
*/
constructor(data?: D) {
super("Unauthorized", -32001, data);
constructor(message = "Unauthorized", data?: D) {
super(message, -32001, data);
}
}

Expand All @@ -40,10 +56,11 @@ export class ForbiddenError<D = unknown> extends RpcError<D> {
/**
* Instantiate a new ForbiddenError.
*
* @param message - Optional message to send with the error. Default: "Forbidden".
* @param data - Optional data to send with the error.
*/
constructor(data?: D) {
super("Forbidden", -32003, data);
constructor(message = "Forbidden", data?: D) {
super(message, -32003, data);
}
}

Expand Down Expand Up @@ -79,9 +96,10 @@ export class NotAcceptableError<D = unknown> extends RpcError<D> {
/**
* Instantiate a new NotAcceptableError.
*
* @param data - Optional data to send with the error.
* @param message - Optional message to send with the error. Default: "Not Acceptable".
* @param data - Optional data to send with the error.
*/
constructor(message: string, data?: D) {
constructor(message = "Not Acceptable", data?: D) {
super(message, -32006, data);
}
}
Expand All @@ -100,9 +118,10 @@ export class ConflictError<D = unknown> extends RpcError<D> {
/**
* Instantiate a new ConflictError.
*
* @param message - Optional message to send with the error. Default: "Conflict".
* @param data - Optional data to send with the error.
*/
constructor(data?: D) {
super("Conflict", -32009, data);
constructor(message = "Conflict", data?: D) {
super(message, -32009, data);
}
}
26 changes: 26 additions & 0 deletions src/Methods/Offer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { api } from "../Api";
import { BadRequestError, method } from "../JsonRpc";
import { utils } from "../Orderbook/Utilities";

api.register<
{
offer: string;
},
{
type: string;
offer: any;
}
>(
"DecodeOffer",
method(async ({ offer }) => {
const psbt = utils.psbt.decode(offer);
if (psbt !== undefined) {
return { type: "psbt", offer: psbt };
}
const tx = utils.raw.decode(offer);
if (tx !== undefined) {
return { type: "raw", offer: tx };
}
throw new BadRequestError();
})
);
20 changes: 20 additions & 0 deletions src/Orderbook/Utilities/Raw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as btc from "bitcoinjs-lib";

export const raw = {
decode,
};

/**
* Attempt to retrieve a raw unsigned transaction from the offer string.
*
* @param offer - Encoded offer transaction.
*
* @returns The raw tx or undefined if it could not be parsed.
*/
function decode(offer: string): any | undefined {
try {
return btc.Transaction.fromHex(offer);
} catch (error) {
return undefined;
}
}
2 changes: 2 additions & 0 deletions src/Orderbook/Utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { PriceList } from "../../Libraries/PriceList";
import { parseLocation } from "../../Libraries/Transaction";
import { Lookup } from "../../Services/Lookup";
import { psbt } from "./PSBT";
import { raw } from "./Raw";

export const utils = {
psbt,
raw,
};

/**
Expand Down
45 changes: 26 additions & 19 deletions src/Orderbook/Validator/Offer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,28 @@ async function hasValidOrder(cid: string): Promise<void> {

async function hasValidOffer({ offer }: IPFSOffer, order: IPFSOrder, lookup: Lookup): Promise<void> {
const psbt = utils.psbt.decode(offer);
if (psbt === undefined) {
return validateRawTx(offer);
if (psbt !== undefined) {
await validateMakerInput(psbt, order.location);
await validateTransactionInputs(psbt, lookup);
return;
}
const raw = validateRawTx(offer);
if (raw === false) {
throw new OfferValidationFailed("Unable to verify offer validity", { offer });
}
await validateMakerInput(psbt, order.location);
await validateTransactionInputs(psbt, lookup);
}

async function validateMakerInput(psbt: btc.Psbt, location: string): Promise<void> {
const [txid, index] = parseLocation(location);
const hasMakerInput = hasOrderInput(psbt, txid, index);
if (hasMakerInput === false) {
const vinIndex = hasOrderInput(psbt, txid, index);
if (vinIndex === false) {
throw new OfferValidationFailed("Offer vin does not include the location specified in the order", {
location,
});
}
if (vinIndex !== 0) {
throw new OfferValidationFailed("Offer location is not the first vin of the transaction", { location });
}
}

async function validateTransactionInputs(psbt: btc.Psbt, lookup: Lookup): Promise<void> {
Expand All @@ -61,12 +68,13 @@ async function validateTransactionInputs(psbt: btc.Psbt, lookup: Lookup): Promis
}
}

function hasOrderInput(psbt: btc.Psbt, txid: string, vout: number): boolean {
function hasOrderInput(psbt: btc.Psbt, txid: string, vout: number): number | false {
for (const input of psbt.data.inputs) {
if (input.nonWitnessUtxo) {
const tx = btc.Transaction.fromBuffer(input.nonWitnessUtxo);
if (tx.getId() === txid && tx.outs.findIndex((_, index) => index === vout) !== -1) {
return true;
const index = tx.outs.findIndex((_, index) => index === vout);
if (tx.getId() === txid && index !== -1) {
return index;
}
}
}
Expand All @@ -88,16 +96,15 @@ function hasOrderInput(psbt: btc.Psbt, txid: string, vout: number): boolean {
*
* @returns `true` if a signature exists in the inputs of the transaction.
*/
export function validateRawTx(offer: string): undefined {
try {
const tx = btc.Transaction.fromHex(offer);
for (const input of tx.ins) {
if (input.script.toString()) {
return;
}
export function validateRawTx(offer: string): boolean {
const tx = utils.raw.decode(offer);
if (tx === undefined) {
throw new OfferValidationFailed("Unable to verify offer validity", { offer });
}
for (const input of tx.ins) {
if (input.script.toString()) {
return true;
}
} catch (error) {
throw new OfferValidationFailed(error.message, { offer });
}
throw new OfferValidationFailed("Unable to verify offer validity", { offer });
return false;
}
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "./Methods/Orderbook";
import "./Methods/IPFS";
import "./Methods/Offer";

import debug from "debug";

Expand Down

0 comments on commit 2fcb465

Please sign in to comment.