Skip to content

Commit a235b55

Browse files
committed
More comprehensive script validation
1 parent c3946a3 commit a235b55

File tree

2 files changed

+256
-39
lines changed

2 files changed

+256
-39
lines changed

src/services/fractionalize/FractionalizeTopicManager.ts

Lines changed: 169 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -109,34 +109,181 @@ export default class FractionalizeTopicManager implements TopicManager {
109109
}
110110
}
111111

112+
// Template sections for comprehensive script validation
112113
const TEMPLATES = {
113-
// Script hexstrings for each type (gotten from scriptHex function below)
114-
"server-token": "00630300000000000000000000000000000000000000005112000000000000000000000000000000000000000000240000000000000000000000000000000000000000686e7ea9140000000000000000000000000000000000000000886b6b516c6c52ae6a200000000000000000000000000000000000000000",
115-
"transfer-token": "006303000000000000000000000000000000000000000051120000000000000000000000000000000000000000002400000000000000000000000000000000000000006876a914000000000000000000000000000000000000000088ac6a200000000000000000000000000000000000000000",
116-
"payment": "6e7ea9140000000000000000000000000000000000000000886b6b516c6c52ae"
114+
"server-token": {
115+
// OP_0 OP_IF 'ord' OP_1 'application/bsv-20' OP_0
116+
formatStart: '0063036f726451126170706c69636174696f6e2f6273762d323000',
117+
// OP_ENDIF OP_2DUP OP_CAT OP_HASH160
118+
formatMiddle: '686e7ea9',
119+
// OP_EQUALVERIFY OP_TOALTSTACK OP_TOALTSTACK OP_1 OP_FROMALTSTACK OP_FROMALTSTACK OP_2 OP_CHECKMULTISIG
120+
formatEnd: '886b6b516c6c52ae'
121+
},
122+
"transfer-token": {
123+
// OP_0 OP_IF 'ord' OP_1 'application/bsv-20' OP_0
124+
formatStart: '0063036f726451126170706c69636174696f6e2f6273762d323000',
125+
// OP_ENDIF OP_DUP OP_HASH160
126+
formatMiddle: '6876a9',
127+
// OP_EQUALVERIFY OP_CHECKSIG
128+
formatEnd: '88ac'
129+
},
130+
"payment": {
131+
// OP_2DUP OP_CAT OP_HASH160
132+
formatStart: '6e7ea9',
133+
// OP_EQUALVERIFY OP_TOALTSTACK OP_TOALTSTACK OP_1 OP_FROMALTSTACK OP_FROMALTSTACK OP_2 OP_CHECKMULTISIG
134+
formatEnd: '886b6b516c6c52ae'
135+
}
117136
}
118-
const NORMALIZED_TEMPLATES = {
119-
"server-token": Script.fromHex(TEMPLATES["server-token"]).toASM(),
120-
"transfer-token": Script.fromHex(TEMPLATES["transfer-token"]).toASM(),
121-
"payment": Script.fromHex(TEMPLATES["payment"]).toASM()
122-
};
123137

124138
function checkScriptFormat(script: Script, type: "server-token" | "transfer-token" | "payment") {
125-
const inputScript = Script.fromHex(script.toHex())
139+
try {
140+
const chunks = script.chunks
126141

127-
// Blank out the data fields to leave us with a standard template
128-
inputScript.chunks.forEach(chunk => {
129-
if (chunk.data && Utils.toHex(chunk.data) !== Utils.toHex([33])) {
130-
chunk.data = Array(20).fill(0)
131-
}
132-
});
142+
switch (type) {
143+
case "server-token": {
144+
// Server token: ordinal inscription + multisig
145+
// Structure: formatStart (0-5) | JSON (6) | formatMiddle (7-10) | hash (11) | formatEnd (12-19) | OP_RETURN (20)
146+
147+
// Check formatStart (chunks 0-5): OP_0 OP_IF 'ord' OP_1 'application/bsv-20' OP_0
148+
const formatStart = new Script(chunks.slice(0, 6)).toHex()
149+
if (formatStart !== TEMPLATES['server-token'].formatStart) {
150+
throw new Error('Malformed formatStart')
151+
}
152+
153+
// Validate JSON payload (chunk 6)
154+
try {
155+
const formatJsonPayload = JSON.parse(Utils.toUTF8(chunks[6].data))
156+
const incorrectlyFormatted =
157+
(formatJsonPayload.p !== 'bsv-20') ||
158+
!(formatJsonPayload.op === 'transfer' || formatJsonPayload.op === 'deploy+mint') ||
159+
formatJsonPayload.amt === undefined ||
160+
(formatJsonPayload.op === 'transfer' && !formatJsonPayload.id)
161+
162+
if (incorrectlyFormatted) {
163+
throw new Error('Malformed JSON payload')
164+
}
165+
} catch (error) {
166+
throw new Error(`Invalid JSON payload: ${error.message}`)
167+
}
168+
169+
// Check formatMiddle (chunks 7-10): OP_ENDIF OP_2DUP OP_CAT OP_HASH160
170+
const formatMiddle = new Script(chunks.slice(7, 11)).toHex()
171+
if (formatMiddle !== TEMPLATES['server-token'].formatMiddle) {
172+
throw new Error('Malformed formatMiddle')
173+
}
174+
175+
// Check hash data (chunk 11): should be 20 bytes
176+
if (!chunks[11].data || chunks[11].data.length !== 20) {
177+
throw new Error('Invalid hash data length')
178+
}
179+
180+
// Check formatEnd (chunks 12-19): multisig ending
181+
const formatEnd = new Script(chunks.slice(12, 20)).toHex()
182+
if (formatEnd !== TEMPLATES['server-token'].formatEnd) {
183+
throw new Error('Malformed formatEnd')
184+
}
185+
186+
// Check OP_RETURN (chunk 20)
187+
if (chunks[20].op !== 106) { // 106 = 0x6a = OP_RETURN
188+
throw new Error('No OP_RETURN at the end')
189+
}
190+
191+
// Validate OP_RETURN contains data (original mint txid)
192+
if (!chunks[20].data || chunks[20].data.length === 0) {
193+
throw new Error('Missing OP_RETURN data')
194+
}
195+
196+
return { valid: true, message: 'Script is valid' }
197+
}
133198

134-
// Check if the script matches the template
135-
// Normalize scripts to ASM to handle pushdata code differences
136-
const isValid = inputScript.toASM() === NORMALIZED_TEMPLATES[type];
199+
case "transfer-token": {
200+
// Transfer token: ordinal inscription + P2PKH
201+
// Structure: formatStart (0-5) | JSON (6) | formatMiddle (7-9) | hash (10) | formatEnd (11-12) | OP_RETURN (13)
202+
203+
// Check formatStart (chunks 0-5)
204+
const formatStart = new Script(chunks.slice(0, 6)).toHex()
205+
if (formatStart !== TEMPLATES['transfer-token'].formatStart) {
206+
throw new Error('Malformed formatStart')
207+
}
208+
209+
// Validate JSON payload (chunk 6)
210+
try {
211+
const formatJsonPayload = JSON.parse(Utils.toUTF8(chunks[6].data))
212+
const incorrectlyFormatted =
213+
(formatJsonPayload.p !== 'bsv-20') ||
214+
!(formatJsonPayload.op === 'transfer' || formatJsonPayload.op === 'deploy+mint') ||
215+
formatJsonPayload.amt === undefined ||
216+
(formatJsonPayload.op === 'transfer' && !formatJsonPayload.id)
217+
218+
if (incorrectlyFormatted) {
219+
throw new Error('Malformed JSON payload')
220+
}
221+
} catch (error) {
222+
throw new Error(`Invalid JSON payload: ${error.message}`)
223+
}
137224

138-
return {
139-
valid: isValid,
140-
message: isValid ? 'Script is valid' : 'Script is invalid'
225+
// Check formatMiddle (chunks 7-9): OP_ENDIF OP_DUP OP_HASH160
226+
const formatMiddle = new Script(chunks.slice(7, 10)).toHex()
227+
if (formatMiddle !== TEMPLATES['transfer-token'].formatMiddle) {
228+
throw new Error('Malformed formatMiddle')
229+
}
230+
231+
// Check pubkey hash data (chunk 10): should be 20 bytes
232+
if (!chunks[10].data || chunks[10].data.length !== 20) {
233+
throw new Error('Invalid pubkey hash data length')
234+
}
235+
236+
// Check formatEnd (chunks 11-12): OP_EQUALVERIFY OP_CHECKSIG
237+
const formatEnd = new Script(chunks.slice(11, 13)).toHex()
238+
if (formatEnd !== TEMPLATES['transfer-token'].formatEnd) {
239+
throw new Error('Malformed formatEnd')
240+
}
241+
242+
// Check OP_RETURN (chunk 13)
243+
if (chunks[13].op !== 106) { // 106 = 0x6a = OP_RETURN
244+
throw new Error('No OP_RETURN at the end')
245+
}
246+
247+
// Validate OP_RETURN contains data (original mint txid)
248+
if (!chunks[13].data || chunks[13].data.length === 0) {
249+
throw new Error('Missing OP_RETURN data')
250+
}
251+
252+
return { valid: true, message: 'Script is valid' }
253+
}
254+
255+
case "payment": {
256+
// Payment script: just multisig, no ordinal inscription
257+
// Structure: formatStart (0-2) | hash (3) | formatEnd (4-11)
258+
259+
// Check formatStart (chunks 0-2): OP_2DUP OP_CAT OP_HASH160
260+
const formatStart = new Script(chunks.slice(0, 3)).toHex()
261+
if (formatStart !== TEMPLATES['payment'].formatStart) {
262+
throw new Error('Malformed formatStart')
263+
}
264+
265+
// Check hash data (chunk 3): should be 20 bytes
266+
if (!chunks[3].data || chunks[3].data.length !== 20) {
267+
throw new Error('Invalid hash data length')
268+
}
269+
270+
// Check formatEnd (chunks 4-11): multisig ending
271+
const formatEnd = new Script(chunks.slice(4, 12)).toHex()
272+
if (formatEnd !== TEMPLATES['payment'].formatEnd) {
273+
throw new Error('Malformed formatEnd')
274+
}
275+
276+
return { valid: true, message: 'Script is valid' }
277+
}
278+
279+
default:
280+
throw new Error(`Unknown script type: ${type}`)
281+
}
282+
283+
} catch (error) {
284+
return {
285+
valid: false,
286+
message: error?.message || 'Invalid script format'
287+
}
141288
}
142289
}

src/services/monsterbattle/MonsterBattleTopicManager.ts

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,14 @@ export default class MonsterBattleTopicManager implements TopicManager {
4646

4747
console.log('[MonsterBattle] Incoming ordinal transaction')
4848

49-
// Check for our ordinal script format
50-
if (output.lockingScript.chunks.length < 14) throw new Error('Invalid locking script error 1')
51-
if (output.lockingScript.chunks[0].op !== OP.OP_0) throw new Error('Invalid locking script error 2')
52-
if (output.lockingScript.chunks[1].op !== OP.OP_IF) throw new Error('Invalid locking script error 3')
53-
if (output.lockingScript.chunks[3].op !== OP.OP_1) throw new Error('Invalid locking script error 4')
54-
if (output.lockingScript.chunks[5].op !== OP.OP_0) throw new Error('Invalid locking script error 5')
55-
if (output.lockingScript.chunks[7].op !== OP.OP_ENDIF) throw new Error('Invalid locking script error 6')
56-
if (output.lockingScript.chunks[8].op !== OP.OP_DUP) throw new Error('Invalid locking script error 7')
57-
if (output.lockingScript.chunks[9].op !== OP.OP_HASH160) throw new Error('Invalid locking script error 8')
58-
if (output.lockingScript.chunks[10].op !== 20) throw new Error('Invalid locking script error 9')
59-
if (output.lockingScript.chunks[11].op !== OP.OP_EQUALVERIFY) throw new Error('Invalid locking script error 10')
60-
if (output.lockingScript.chunks[12].op !== OP.OP_CHECKSIG) throw new Error('Invalid locking script error 11')
61-
if (output.lockingScript.chunks[13].op !== OP.OP_RETURN) throw new Error('Invalid locking script error 12')
62-
63-
console.log(`[MonsterBattle] Ordinal transaction passed checks`)
64-
65-
outputsToAdmit.push(index)
49+
// Check for ordinal inscription format
50+
const result = checkScriptFormat(output.lockingScript)
51+
if (result.valid) {
52+
console.log(`[MonsterBattle] Ordinal transaction passed checks`)
53+
outputsToAdmit.push(index)
54+
} else {
55+
console.log(`[MonsterBattle] Ordinal validation failed: ${result.message}`)
56+
}
6657
} catch (err) {
6758
console.error(`Error processing output ${index}:`, err)
6859
// Continue with next output
@@ -113,6 +104,85 @@ export default class MonsterBattleTopicManager implements TopicManager {
113104
}
114105
}
115106

107+
// Template sections for ordinal inscription validation
108+
// Structure: formatStart (0-5) | JSON (6) | formatMiddle (7-9) | hash (10) | formatEnd (11-12) | OP_RETURN (13)
109+
const TEMPLATES = {
110+
// OP_0 OP_IF 'ord' OP_1 'application/bsv-21' OP_0
111+
formatStart: '0063036f726451126170706c69636174696f6e2f6273762d323100',
112+
// OP_ENDIF OP_DUP OP_HASH160
113+
formatMiddle: '6876a9',
114+
// OP_EQUALVERIFY OP_CHECKSIG
115+
formatEnd: '88ac'
116+
}
117+
118+
function checkScriptFormat(script: Script) {
119+
try {
120+
const chunks = script.chunks
121+
122+
// Check minimum chunk count
123+
if (chunks.length < 14) {
124+
throw new Error('Insufficient chunks in script')
125+
}
126+
127+
// Check formatStart (chunks 0-5): OP_0 OP_IF 'ord' OP_1 'application/bsv-21' OP_0
128+
const formatStart = new Script(chunks.slice(0, 6)).toHex()
129+
if (formatStart !== TEMPLATES.formatStart) {
130+
throw new Error('Malformed formatStart')
131+
}
132+
133+
// Validate JSON payload (chunk 6)
134+
try {
135+
const formatJsonPayload = JSON.parse(Utils.toUTF8(chunks[6].data))
136+
const incorrectlyFormatted =
137+
(formatJsonPayload.p !== 'bsv-20') ||
138+
!(formatJsonPayload.op === 'transfer' || formatJsonPayload.op === 'deploy+mint') ||
139+
formatJsonPayload.amt === undefined ||
140+
(formatJsonPayload.op === 'transfer' && !formatJsonPayload.id)
141+
142+
if (incorrectlyFormatted) {
143+
throw new Error('Malformed JSON payload')
144+
}
145+
} catch (error) {
146+
throw new Error(`Invalid JSON payload: ${error.message}`)
147+
}
148+
149+
// Check formatMiddle (chunks 7-9): OP_ENDIF OP_DUP OP_HASH160
150+
const formatMiddle = new Script(chunks.slice(7, 10)).toHex()
151+
if (formatMiddle !== TEMPLATES.formatMiddle) {
152+
throw new Error('Malformed formatMiddle')
153+
}
154+
155+
// Check pubkey hash data (chunk 10): should be 20 bytes
156+
if (!chunks[10].data || chunks[10].data.length !== 20) {
157+
throw new Error('Invalid pubkey hash data length')
158+
}
159+
160+
// Check formatEnd (chunks 11-12): OP_EQUALVERIFY OP_CHECKSIG
161+
const formatEnd = new Script(chunks.slice(11, 13)).toHex()
162+
if (formatEnd !== TEMPLATES.formatEnd) {
163+
throw new Error('Malformed formatEnd')
164+
}
165+
166+
// Check OP_RETURN (chunk 13)
167+
if (chunks[13].op !== 106) { // 106 = 0x6a = OP_RETURN
168+
throw new Error('No OP_RETURN at the end')
169+
}
170+
171+
// Validate OP_RETURN contains data
172+
if (!chunks[13].data || chunks[13].data.length === 0) {
173+
throw new Error('Missing OP_RETURN data')
174+
}
175+
176+
return { valid: true, message: 'Script is valid' }
177+
178+
} catch (error) {
179+
return {
180+
valid: false,
181+
message: error?.message || 'Invalid script format'
182+
}
183+
}
184+
}
185+
116186
// Helper to check if script is P2PKH
117187
function isP2PKH(script: Script): boolean {
118188
const chunks = script.chunks;

0 commit comments

Comments
 (0)