Add transaction check logic for document validation
This commit is contained in:
parent
73cee5d144
commit
72d43210de
@ -8,6 +8,40 @@ interface State {
|
|||||||
commitmentHashes: string[];
|
commitmentHashes: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Vin {
|
||||||
|
txid: string; // The txid of the previous transaction (being spent)
|
||||||
|
vout: number; // The output index in the previous tx
|
||||||
|
prevout: {
|
||||||
|
scriptpubkey: string;
|
||||||
|
scriptpubkey_asm: string;
|
||||||
|
scriptpubkey_type: string;
|
||||||
|
scriptpubkey_address: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
scriptsig: string;
|
||||||
|
scriptsig_asm: string;
|
||||||
|
witness: string[];
|
||||||
|
is_coinbase: boolean;
|
||||||
|
sequence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionInfo {
|
||||||
|
txid: string;
|
||||||
|
version: number;
|
||||||
|
locktime: number;
|
||||||
|
vin: Vin[];
|
||||||
|
vout: any[];
|
||||||
|
size: number;
|
||||||
|
weight: number;
|
||||||
|
fee: number;
|
||||||
|
status: {
|
||||||
|
confirmed: boolean;
|
||||||
|
block_height: number;
|
||||||
|
block_hash: string;
|
||||||
|
block_time: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getDocumentValidation(container: HTMLElement) {
|
export function getDocumentValidation(container: HTMLElement) {
|
||||||
const state: State = {
|
const state: State = {
|
||||||
file: null,
|
file: null,
|
||||||
@ -170,7 +204,80 @@ export function getDocumentValidation(container: HTMLElement) {
|
|||||||
};
|
};
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
const commitedIn = state.certificate.commited_in;
|
const commitedIn = state.certificate.commited_in;
|
||||||
|
if (!commitedIn) return;
|
||||||
|
const [prevTxid, prevTxVout] = commitedIn.split(':');
|
||||||
|
const processId = state.certificate.process_id;
|
||||||
|
const stateId = state.certificate.state_id;
|
||||||
|
const process = await service.getProcess(processId);
|
||||||
|
if (!process) return;
|
||||||
|
|
||||||
|
// Get the transaction that comes right after the commited_in
|
||||||
|
const nextState = service.getNextStateAfterId(process, stateId);
|
||||||
|
|
||||||
|
if (!nextState) {
|
||||||
|
alert(`❌ Validation failed: No next state, is the state you're trying to validate commited?`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [outspentTxId, _] = nextState.commited_in.split(':');
|
||||||
|
console.log(outspentTxId);
|
||||||
|
|
||||||
|
// Check that the commitment transaction exists, and that it commits to the state id
|
||||||
|
|
||||||
|
const txInfo = await fetchTransaction(outspentTxId);
|
||||||
|
if (!txInfo) {
|
||||||
|
console.error(`Validation error: Can't fetch new state commitment transaction`);
|
||||||
|
alert(`❌ Validation failed: invalid or non existent commited_in for state ${stateId}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must check that this transaction indeed spend the commited_in we have in the certificate
|
||||||
let found = false;
|
let found = false;
|
||||||
|
for (const vin of txInfo.vin) {
|
||||||
|
if (vin.txid === prevTxid) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
console.error(`Validation error: new state doesn't spend previous state commitment transaction`);
|
||||||
|
alert('❌ Validation failed: Unconsistent commitment transactions history.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set found back to false for next check
|
||||||
|
found = false;
|
||||||
|
|
||||||
|
// is the state_id commited in the transaction?
|
||||||
|
for (const vout of txInfo.vout) {
|
||||||
|
console.log(vout);
|
||||||
|
if (vout.scriptpubkey_type && vout.scriptpubkey_type === 'op_return') {
|
||||||
|
found = true;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vout.scriptpubkey_asm) {
|
||||||
|
const hash = extractHexFromScriptAsm(vout.scriptpubkey_asm);
|
||||||
|
if (hash) {
|
||||||
|
if (hash !== stateId) {
|
||||||
|
console.error(`Validation error: expected stateId ${stateId}, got ${hash}`);
|
||||||
|
alert('❌ Validation failed: Transaction does not commit to that state.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
alert('❌ Validation failed: Transaction does not contain data.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set found back to false for next check
|
||||||
|
found = false;
|
||||||
|
|
||||||
for (const label of Object.keys(state.certificate.pcd_commitment)) {
|
for (const label of Object.keys(state.certificate.pcd_commitment)) {
|
||||||
// Compute the hash for this label
|
// Compute the hash for this label
|
||||||
console.log(`Computing hash with label ${label}`)
|
console.log(`Computing hash with label ${label}`)
|
||||||
@ -187,4 +294,28 @@ export function getDocumentValidation(container: HTMLElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchTransaction(txid: string): Promise<TransactionInfo> {
|
||||||
|
const url = `https://mempool.4nkweb.com/api/tx/${txid}`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch outspend status: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outspend: TransactionInfo = await response.json();
|
||||||
|
return outspend;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHexFromScriptAsm(scriptAsm: string): string | null {
|
||||||
|
const parts = scriptAsm.trim().split(/\s+/);
|
||||||
|
const last = parts[parts.length - 1];
|
||||||
|
|
||||||
|
// Basic validation: must be 64-char hex (32 bytes)
|
||||||
|
if (/^[0-9a-fA-F]{64}$/.test(last)) {
|
||||||
|
return last.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ export async function getProcessCreation(container: HTMLElement) {
|
|||||||
await service.handleApiReturn(approveChangeResult);
|
await service.handleApiReturn(approveChangeResult);
|
||||||
if (approveChangeResult) {
|
if (approveChangeResult) {
|
||||||
const process = await service.getProcess(processId);
|
const process = await service.getProcess(processId);
|
||||||
const newState = service.getStateFromId(process, stateId);
|
let newState = service.getStateFromId(process, stateId);
|
||||||
if (!newState) return;
|
if (!newState) return;
|
||||||
for (const label of Object.keys(newState.keys)) {
|
for (const label of Object.keys(newState.keys)) {
|
||||||
const hash = newState.pcd_commitment[label];
|
const hash = newState.pcd_commitment[label];
|
||||||
@ -70,6 +70,8 @@ export async function getProcessCreation(container: HTMLElement) {
|
|||||||
|
|
||||||
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
||||||
}
|
}
|
||||||
|
// Add processId to the state we export
|
||||||
|
newState['process_id'] = processId;
|
||||||
const blob = new Blob([JSON.stringify(newState, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(newState, null, 2)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
@ -1779,6 +1779,18 @@ export default class Services {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getNextStateAfterId(process: Process, stateId: string): ProcessState | null {
|
||||||
|
if (process.states.length === 0) return null;
|
||||||
|
|
||||||
|
const index = process.states.findIndex(state => state.state_id === stateId);
|
||||||
|
|
||||||
|
if (index !== -1 && index < process.states.length - 1) {
|
||||||
|
return process.states[index + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public isPairingProcess(roles: Record<string, RoleDefinition>): boolean {
|
public isPairingProcess(roles: Record<string, RoleDefinition>): boolean {
|
||||||
if (Object.keys(roles).length != 1) { return false }
|
if (Object.keys(roles).length != 1) { return false }
|
||||||
const pairingRole = roles['pairing'];
|
const pairingRole = roles['pairing'];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user