1use crate::error::{Context, Error, bail, ensure};
2use crate::{
3 container,
4 format::{PublicRecord, RecordKind, Urma},
5 multipart::{Geometry, MultipartRecord},
6};
7use bitcoin::{
8 Script, ScriptBuf, Transaction, TxOut, Witness, XOnlyPublicKey,
9 hashes::Hash,
10 opcodes::all::{OP_CHECKSIG, OP_ENDIF, OP_IF},
11 script::{Builder, Instruction, PushBytesBuf},
12 secp256k1::{Keypair, Message, Secp256k1},
13 sighash::{Prevouts, SighashCache, TapSighashType},
14 taproot::{ControlBlock, LeafVersion, TapLeafHash, TaprootBuilder, TaprootSpendInfo},
15 transaction::Version,
16};
17
18pub struct ParsedEnvelope {
19 pub author: XOnlyPublicKey,
20 pub record: Vec<u8>,
21 pub script: ScriptBuf,
22 pub control: ControlBlock,
23}
24
25fn validate_record(record: &[u8]) -> Result<(), Error> {
26 match RecordKind::parse(record)? {
27 RecordKind::Private => {
28 container::inspect_header(record)?;
29 }
30 RecordKind::DataPart | RecordKind::LeafManifest | RecordKind::RootManifest => {
31 MultipartRecord::decode(record)?;
32 }
33 RecordKind::Container => bail!("offline container cannot be published as a record"),
34 RecordKind::Post
35 | RecordKind::Reply
36 | RecordKind::Profile
37 | RecordKind::Avatar
38 | RecordKind::WirePost
39 | RecordKind::WireReply
40 | RecordKind::ProfileRecord => {
41 PublicRecord::decode(record)?;
42 }
43 }
44 Ok(())
45}
46
47fn record_script(record: &[u8], author: XOnlyPublicKey) -> Result<ScriptBuf, Error> {
48 let mut builder = Builder::new()
49 .push_x_only_key(&author)
50 .push_opcode(OP_CHECKSIG)
51 .push_int(0)
52 .push_opcode(OP_IF)
53 .push_slice(Urma::MAGIC);
54 for part in record.chunks(520) {
55 builder = match part {
56 [value @ 1..=16] => builder.push_int(i64::from(*value)),
57 [0x81] => builder.push_int(-1),
58 bytes => builder.push_slice(PushBytesBuf::try_from(bytes.to_vec())?),
59 };
60 }
61 Ok(builder.push_opcode(OP_ENDIF).into_script())
62}
63
64pub fn build(record: &[u8], signer: &Keypair) -> Result<(ScriptBuf, TaprootSpendInfo), Error> {
65 build_for_author(record, signer.x_only_public_key().0)
66}
67
68pub fn build_for_author(
69 record: &[u8],
70 author: XOnlyPublicKey,
71) -> Result<(ScriptBuf, TaprootSpendInfo), Error> {
72 validate_record(record)?;
73 let script = record_script(record, author)?;
74 let info = TaprootBuilder::new()
75 .add_leaf(0, script.clone())?
76 .finalize(&Secp256k1::new(), author)
77 .map_err(|tree| Error::Invalid(format!("invalid single-leaf Taproot tree: {tree:?}")))?;
78 Ok((script, info))
79}
80
81pub fn witness(
82 signature: &[u8],
83 script: &ScriptBuf,
84 info: &TaprootSpendInfo,
85) -> Result<Witness, Error> {
86 ensure!(
87 signature.len() == 64,
88 "URMA requires implicit SIGHASH_DEFAULT"
89 );
90 let control = info
91 .control_block(&(script.clone(), LeafVersion::TapScript))
92 .context("script missing from Taproot tree")?
93 .serialize();
94 ensure!(control.len() == 33, "URMA requires one leaf");
95 Ok(Witness::from_slice(&[
96 signature,
97 script.as_bytes(),
98 &control,
99 ]))
100}
101
102pub fn is_candidate(witness: &Witness) -> bool {
103 if witness.len() != 3 {
104 return false;
105 }
106 let Some(script) = witness.iter().nth(1) else {
107 return false;
108 };
109 script.len() >= 41 && script[34..41] == [0, 99, 4, b'U', b'R', b'M', b'A']
110}
111
112pub fn extract(witness: &Witness) -> Result<ParsedEnvelope, Error> {
113 let parsed = extract_envelope(witness)?;
114 validate_record(&parsed.record)?;
115 Ok(parsed)
116}
117
118fn extract_envelope(witness: &Witness) -> Result<ParsedEnvelope, Error> {
119 ensure!(witness.len() == 3, "invalid witness item count");
120 let items: Vec<_> = witness.iter().collect();
121 ensure!(
122 items[0].len() == 64 && items[2].len() == 33,
123 "invalid signature or control length"
124 );
125 ensure!(
126 items[1].len() >= 42
127 && items[1].len()
128 <= Geometry::RECORD_BYTES + 3 * Geometry::RECORD_BYTES.div_ceil(520) + 42,
129 "script size out of bounds"
130 );
131 let script = ScriptBuf::from_bytes(items[1].to_vec());
132 let author = XOnlyPublicKey::from_slice(&items[1][1..33])?;
133 let control = ControlBlock::decode(items[2])?;
134 ensure!(
135 control.leaf_version == LeafVersion::TapScript && control.internal_key == author,
136 "invalid author/internal-key commitment"
137 );
138 let mut ops = script.instructions_minimal().skip(5);
139 let mut record = Vec::new();
140 loop {
141 let instruction = ops.next().context("missing envelope end")??;
142 match instruction {
143 Instruction::PushBytes(part) => {
144 ensure!(
145 !part.is_empty() && part.len() <= 520,
146 "invalid record segment"
147 );
148 record.extend_from_slice(part.as_bytes());
149 }
150 Instruction::Op(opcode) if opcode == OP_ENDIF => {
151 let Some(instruction) = ops.next() else { break };
152 bail!("trailing script instruction: {instruction:?}");
153 }
154 Instruction::Op(opcode) if (0x51..=0x60).contains(&opcode.to_u8()) => {
155 record.push(opcode.to_u8() - 0x50)
156 }
157 Instruction::Op(opcode) if opcode.to_u8() == 0x4f => record.push(0x81),
158 instruction => bail!("invalid envelope instruction {instruction:?}"),
159 }
160 ensure!(record.len() <= Geometry::RECORD_BYTES, "record too large");
161 }
162 ensure!(
163 record_script(&record, author)? == script,
164 "noncanonical envelope or segmentation"
165 );
166 Ok(ParsedEnvelope {
167 author,
168 record,
169 script,
170 control,
171 })
172}
173
174pub fn verify_reveal(reveal: &Transaction, commit: &Transaction) -> Result<ParsedEnvelope, Error> {
175 let parsed = verify_record_proof(reveal, commit)?;
176 validate_record(&parsed.record)?;
177 Ok(parsed)
178}
179
180pub(crate) fn verify_record_proof(
181 reveal: &Transaction,
182 commit: &Transaction,
183) -> Result<ParsedEnvelope, Error> {
184 ensure!(reveal.input.len() == 1, "reveal must have one input");
185 let source = reveal.input[0].previous_output;
186 ensure!(
187 source.txid == commit.compute_txid(),
188 "reveal does not spend the supplied commit"
189 );
190 let prevout = commit
191 .output
192 .get(usize::try_from(source.vout)?)
193 .context("commit outpoint absent")?;
194 verify_prevout_proof(reveal, prevout)
195}
196
197pub fn verify_prevout(reveal: &Transaction, prevout: &TxOut) -> Result<ParsedEnvelope, Error> {
198 let parsed = verify_prevout_proof(reveal, prevout)?;
199 validate_record(&parsed.record)?;
200 Ok(parsed)
201}
202
203fn verify_prevout_proof(reveal: &Transaction, prevout: &TxOut) -> Result<ParsedEnvelope, Error> {
204 let parsed = extract_reveal_envelope(reveal)?;
205 ensure!(
206 reveal.output[0].value <= prevout.value,
207 "reveal spends more than prevout"
208 );
209 ensure!(
210 prevout.script_pubkey.is_p2tr(),
211 "commit output must be P2TR"
212 );
213 let secp = Secp256k1::verification_only();
214 let output_key = XOnlyPublicKey::from_slice(&prevout.script_pubkey.as_bytes()[2..34])?;
215 ensure!(
216 parsed
217 .control
218 .verify_taproot_commitment(&secp, output_key, &parsed.script),
219 "invalid Taproot commitment"
220 );
221 let signature = bitcoin::secp256k1::schnorr::Signature::from_slice(
222 reveal.input[0]
223 .witness
224 .iter()
225 .next()
226 .context("signature absent")?,
227 )?;
228 let hash = SighashCache::new(reveal).taproot_script_spend_signature_hash(
229 0,
230 &Prevouts::All(std::slice::from_ref(prevout)),
231 TapLeafHash::from_script(&parsed.script, LeafVersion::TapScript),
232 TapSighashType::Default,
233 )?;
234 secp.verify_schnorr(
235 &signature,
236 &Message::from_digest(hash.to_byte_array()),
237 &parsed.author,
238 )
239 .context("invalid author signature")?;
240 Ok(parsed)
241}
242pub fn extract_reveal(reveal: &Transaction) -> Result<ParsedEnvelope, Error> {
243 let parsed = extract_reveal_envelope(reveal)?;
244 validate_record(&parsed.record)?;
245 Ok(parsed)
246}
247
248fn extract_reveal_envelope(reveal: &Transaction) -> Result<ParsedEnvelope, Error> {
249 validate_reveal_shape(reveal.version.0, reveal.input.len(), reveal.output.len())?;
250 validate_reveal_scripts(&reveal.input[0].script_sig, &reveal.output[0].script_pubkey)?;
251 extract_envelope(&reveal.input[0].witness)
252}
253
254pub fn validate_reveal_shape(version: i32, inputs: usize, outputs: usize) -> Result<(), Error> {
255 ensure!(
256 version == Version::TWO.0 && inputs == 1 && outputs == 1,
257 "invalid reveal transaction shape"
258 );
259 Ok(())
260}
261
262pub fn validate_reveal_scripts(script_sig: &Script, return_script: &Script) -> Result<(), Error> {
263 ensure!(script_sig.is_empty(), "reveal scriptSig must be empty");
264 ensure!(
265 return_script.is_p2wpkh() || return_script.is_p2tr(),
266 "invalid reveal return output"
267 );
268 Ok(())
269}