Add signature annotations to legacy scriptsigs

This commit is contained in:
Mononaut 2025-04-17 05:57:12 +00:00
parent 29b0fb9b3b
commit f83e35699e
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
7 changed files with 284 additions and 12 deletions

View File

@ -155,7 +155,14 @@
<ng-template [ngIf]="vin.scriptsig">
<tr>
<td i18n="transactions-list.scriptsig.asm|ScriptSig (ASM)">ScriptSig (ASM)</td>
<td style="text-align: left;" [innerHTML]="vin.scriptsig_asm | asmStyler"></td>
<td style="text-align: left;">
<app-asm
[asm]="vin.scriptsig_asm"
[annotations]="{ signatures: tx['_sigmap'], selectedSig: selectedSig?.txIndex === i && selectedSig?.vindex === vindex ? selectedSig.sig : null }"
(showSigInfo)="showSigInfo(i, vindex, $event)"
(hideSigInfo)="hideSigInfo()"
></app-asm>
</td>
</tr>
<tr>
<td i18n="transactions-list.scriptsig.hex|ScriptSig (HEX)">ScriptSig (HEX)</td>

View File

@ -15,7 +15,7 @@ import { OrdApiService } from '@app/services/ord-api.service';
import { Inscription } from '@app/shared/ord/inscription.utils';
import { Etching, Runestone } from '@app/shared/ord/rune.utils';
import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-utils';
import { processInputSignatures, Sighash, SigInfo } from '../../shared/transaction.utils';
import { processInputSignatures, Sighash, SigInfo, SighashLabels } from '@app/shared/transaction.utils';
@Component({
selector: 'app-transactions-list',
@ -62,15 +62,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
selectedSig: { txIndex: number, vindex: number, sig: SigInfo } | null = null;
sigHighlights: { vin: boolean[], vout: boolean[] } = { vin: [], vout: [] };
sighashLabels: { [sighash: string]: string } = {
'0': 'SIGHASH_DEFAULT',
'1': 'SIGHASH_ALL',
'2': 'SIGHASH_NONE',
'3': 'SIGHASH_SINGLE',
'129': 'SIGHASH_ALL | ACP',
'130': 'SIGHASH_NONE | ACP',
'131': 'SIGHASH_SINGLE | ACP',
};
sighashLabels = SighashLabels;
constructor(
public stateService: StateService,

View File

@ -0,0 +1,30 @@
<div class="asm">
@for (instruction of instructions; track instruction.instruction) {
<span [class]='opcodeStyles.get(instruction.instruction)'>OP_{{instruction.instruction}}</span>
@for (arg of instruction.args; track arg) {
<ng-container *ngIf="annotations.signatures[arg] as sigInfo; else plainArg">
<span class="sig sig-key sig-inline sighash-{{sigInfo.sig.sighash}}"
[class.hovered]="annotations.selectedSig && annotations.selectedSig === sigInfo.sig"
(mouseenter)="doShowSigInfo(sigInfo.sig)"
(mouseleave)="doHideSigInfo()"
[title]="sighashLabels[sigInfo.sig.sighash]">
<fa-icon [icon]="['fas', 'key']" [fixedWidth]="true"></fa-icon>
</span>
<ng-container *ngIf="sigInfo.sig.sighash !== 0; else plainSig">
{{arg.slice(0, -2)}}<span class="sig sighash-{{sigInfo.sig.sighash}}"
[class.hovered]="annotations.selectedSig && annotations.selectedSig === sigInfo.sig"
(mouseenter)="doShowSigInfo(sigInfo.sig)"
(mouseleave)="doHideSigInfo()"
[title]="sighashLabels[sigInfo.sig.sighash]">{{arg.slice(-2)}}</span>
</ng-container>
<ng-template #plainSig>
{{ arg }}
</ng-template>
</ng-container>
<ng-template #plainArg>
{{ arg }}
</ng-template>
}
<br>
}
</div>

View File

@ -0,0 +1,53 @@
.sig {
cursor: pointer;
&.sighash-0 {
--sigColor: var(--green);
}
&.sighash-1 {
--sigColor: var(--green);
}
&.sighash-2 {
--sigColor: gold;
}
&.sighash-3 {
--sigColor: cornflowerblue;
}
&.sighash-129 {
--sigColor: darkviolet;
}
&.sighash-130 {
--sigColor: darkorange;
}
&.sighash-131 {
--sigColor: var(--pink);
}
&.sig-no-lock {
--sigColor: var(--red);
}
color: var(--sigColor);
&:hover, &.hovered {
color: color-mix(in srgb, var(--sigColor) 60%, white);
}
::ng-deep svg path {
pointer-events: auto;
}
}
.sig-key {
position: absolute;
top: 0;
left: 0;
transition: transform 0.2s ease;
pointer-events: none;
&.sig-inline {
position: relative;
pointer-events: auto;
margin-left: 4px;
margin-right: -4px;
}
}

View File

@ -0,0 +1,178 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
import { SigInfo, SighashLabels } from '@app/shared/transaction.utils';
@Component({
selector: 'app-asm',
templateUrl: './asm.component.html',
styleUrls: ['./asm.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AsmComponent {
@Input() asm: string;
@Input() crop: number = 0;
@Input() annotations: {
signatures: Record<string, { sig: SigInfo, vindex: number }>,
selectedSig: SigInfo | null
} = {
signatures: {},
selectedSig: null
};
@Output() showSigInfo = new EventEmitter<SigInfo>();
@Output() hideSigInfo = new EventEmitter<void>();
instructions: { instruction: string, args: string[] }[] = [];
sighashLabels: Record<number, string> = SighashLabels;
ngOnInit(): void {
this.parseASM();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['asm']) {
this.parseASM();
}
}
parseASM(): void {
let instructions = this.asm.split('OP_');
// trim instructions to a whole number of instructions with at most `crop` characters total
if (this.crop) {
let chars = 0;
for (let i = 0; i < instructions.length; i++) {
chars += instructions[i].length + 3;
if (chars > this.crop) {
instructions = instructions.slice(0, i);
break;
}
}
}
this.instructions = instructions.filter(instruction => instruction.trim() !== '').map(instruction => {
const parts = instruction.split(' ');
return {
instruction: parts[0],
args: parts.slice(1)
};
});
}
doShowSigInfo(sig: SigInfo): void {
this.showSigInfo.emit(sig);
}
doHideSigInfo(): void {
this.hideSigInfo.emit();
}
readonly opcodeStyles: Map<string, string> = new Map([
// Constants
['0', 'constants'],
['FALSE', 'constants'],
['TRUE', 'constants'],
...Array.from({length: 75}, (_, i) => [`PUSHBYTES_${i + 1}`, 'constants']),
['PUSHDATA1', 'constants'],
['PUSHDATA2', 'constants'],
['PUSHDATA4', 'constants'],
['PUSHNUM_NEG1', 'constants'],
...Array.from({length: 16}, (_, i) => [`PUSHNUM_${i + 1}`, 'constants']),
// Control flow
['NOP', 'control'],
['IF', 'control'],
['NOTIF', 'control'],
['ELSE', 'control'],
['ENDIF', 'control'],
['VERIFY', 'control'],
...Array.from({length: 70}, (_, i) => [`RETURN_${i + 186}`, 'control']),
// Stack
['TOALTSTACK', 'stack'],
['FROMALTSTACK', 'stack'],
['IFDUP', 'stack'],
['DEPTH', 'stack'],
['DROP', 'stack'],
['DUP', 'stack'],
['NIP', 'stack'],
['OVER', 'stack'],
['PICK', 'stack'],
['ROLL', 'stack'],
['ROT', 'stack'],
['SWAP', 'stack'],
['TUCK', 'stack'],
['2DROP', 'stack'],
['2DUP', 'stack'],
['3DUP', 'stack'],
['2OVER', 'stack'],
['2ROT', 'stack'],
['2SWAP', 'stack'],
// String
['CAT', 'splice'],
['SUBSTR', 'splice'],
['LEFT', 'splice'],
['RIGHT', 'splice'],
['SIZE', 'splice'],
// Logic
['INVERT', 'logic'],
['AND', 'logic'],
['OR', 'logic'],
['XOR', 'logic'],
['EQUAL', 'logic'],
['EQUALVERIFY', 'logic'],
// Arithmetic
['1ADD', 'arithmetic'],
['1SUB', 'arithmetic'],
['2MUL', 'arithmetic'],
['2DIV', 'arithmetic'],
['NEGATE', 'arithmetic'],
['ABS', 'arithmetic'],
['NOT', 'arithmetic'],
['0NOTEQUAL', 'arithmetic'],
['ADD', 'arithmetic'],
['SUB', 'arithmetic'],
['MUL', 'arithmetic'],
['DIV', 'arithmetic'],
['MOD', 'arithmetic'],
['LSHIFT', 'arithmetic'],
['RSHIFT', 'arithmetic'],
['BOOLAND', 'arithmetic'],
['BOOLOR', 'arithmetic'],
['NUMEQUAL', 'arithmetic'],
['NUMEQUALVERIFY', 'arithmetic'],
['NUMNOTEQUAL', 'arithmetic'],
['LESSTHAN', 'arithmetic'],
['GREATERTHAN', 'arithmetic'],
['LESSTHANOREQUAL', 'arithmetic'],
['GREATERTHANOREQUAL', 'arithmetic'],
['MIN', 'arithmetic'],
['MAX', 'arithmetic'],
['WITHIN', 'arithmetic'],
// Crypto
['RIPEMD160', 'crypto'],
['SHA1', 'crypto'],
['SHA256', 'crypto'],
['HASH160', 'crypto'],
['HASH256', 'crypto'],
['CODESEPARATOR', 'crypto'],
['CHECKSIG', 'crypto'],
['CHECKSIGVERIFY', 'crypto'],
['CHECKMULTISIG', 'crypto'],
['CHECKMULTISIGVERIFY', 'crypto'],
['CHECKSIGADD', 'crypto'],
// Locktime
['CLTV', 'locktime'],
['CSV', 'locktime'],
// Reserved
['RESERVED', 'reserved'],
['VER', 'reserved'],
['VERIF', 'reserved'],
['VERNOTIF', 'reserved'],
['RESERVED1', 'reserved'],
['RESERVED2', 'reserved'],
...Array.from({length: 10}, (_, i) => [`NOP${i + 1}`, 'reserved'])
] as [string, string][]);
}

View File

@ -18,6 +18,7 @@ import { Hex2asciiPipe } from '@app/shared/pipes/hex2ascii/hex2ascii.pipe';
import { Decimal2HexPipe } from '@app/shared/pipes/decimal2hex/decimal2hex.pipe';
import { FeeRoundingPipe } from '@app/shared/pipes/fee-rounding/fee-rounding.pipe';
import { AsmStylerPipe } from '@app/shared/pipes/asm-styler/asm-styler.pipe';
import { AsmComponent } from '@app/shared/components/asm/asm.component';
import { AbsolutePipe } from '@app/shared/pipes/absolute/absolute.pipe';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { ScriptpubkeyTypePipe } from '@app/shared/pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe';
@ -147,6 +148,7 @@ import { GithubLogin } from '../components/github-login.component/github-login.c
NoSanitizePipe,
Hex2asciiPipe,
AsmStylerPipe,
AsmComponent,
AbsolutePipe,
BytesPipe,
VbytesPipe,

View File

@ -104,6 +104,16 @@ export type SighashValue =
(SighashFlag.SINGLE & SighashFlag.ANYONECANPAY) |
(SighashFlag.ALL & SighashFlag.NONE);
export const SighashLabels: Record<number, string> = {
'0': 'SIGHASH_DEFAULT',
'1': 'SIGHASH_ALL',
'2': 'SIGHASH_NONE',
'3': 'SIGHASH_SINGLE',
'129': 'SIGHASH_ALL | ACP',
'130': 'SIGHASH_NONE | ACP',
'131': 'SIGHASH_SINGLE | ACP',
};
export interface SigInfo {
signature: string;
sighash: SighashValue;
@ -172,7 +182,7 @@ export function extractDERSignaturesASM(script_asm: string): SigInfo[] {
if (isDERSig(hexData)) {
const sighash = decodeSighashFlag(parseInt(hexData.slice(-2), 16));
signatures.push({
signature: hexData.slice(0, -2), // Remove sighash byte
signature: hexData,
sighash
});
}