1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
use std::{convert::TryInto, fmt};
use bitcoin::{
prelude::{Transaction, TransactionDecodeError},
Decodable,
};
use bitcoin_client::{BitcoinClient, HttpClient, HttpsClient, NodeError};
use hyper::{Body, Request as HttpRequest, Response as HttpResponse};
use ring::digest::{Context, SHA256};
use thiserror::Error;
use tower_service::Service;
#[derive(Debug, Error)]
pub enum ValidationError<E: fmt::Debug + fmt::Display + 'static> {
#[error("failed to decode token: {0}")]
Base64(base64::DecodeError),
#[error("unexpected script length")]
IncorrectLength,
#[error("invalid token")]
Invalid,
#[error(transparent)]
Node(NodeError<E>),
#[error("output is not an op return format")]
NotOpReturn,
#[error("output missing")]
OutputNotFound,
#[error("failed to decode transaction: {0}")]
Transaction(TransactionDecodeError),
#[error("unexpected token length")]
TokenLength,
}
#[derive(Clone, Debug)]
pub struct ChainCommitmentScheme<S> {
client: BitcoinClient<S>,
}
const COMMITMENT_LEN: usize = 32;
pub fn construct_commitment(pub_key_hash: &[u8], address_metadata_hash: &[u8]) -> Vec<u8> {
let mut sha256_context = Context::new(&SHA256);
sha256_context.update(pub_key_hash);
sha256_context.update(address_metadata_hash);
sha256_context.finish().as_ref().to_vec()
}
pub fn construct_token_raw(tx_id: &[u8], vout: u32) -> Vec<u8> {
[tx_id, &vout.to_le_bytes()[..]].concat()
}
pub fn construct_token(tx_id: &[u8], vout: u32) -> String {
let raw_token = construct_token_raw(tx_id, vout);
let url_safe_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
base64::encode_config(raw_token, url_safe_config)
}
impl<S> ChainCommitmentScheme<S> {
pub fn from_client(client: BitcoinClient<S>) -> Self {
ChainCommitmentScheme { client }
}
}
impl ChainCommitmentScheme<HttpClient> {
pub fn new(endpoint: String, username: String, password: String) -> Self {
Self {
client: BitcoinClient::new(endpoint, username, password),
}
}
}
impl ChainCommitmentScheme<HttpsClient> {
pub fn new_tls(endpoint: String, username: String, password: String) -> Self {
Self {
client: BitcoinClient::new_tls(endpoint, username, password),
}
}
}
impl<S> ChainCommitmentScheme<S>
where
S: Service<HttpRequest<Body>, Response = HttpResponse<Body>> + Clone,
S::Error: fmt::Debug + fmt::Display + 'static,
S::Future: Send + 'static,
{
pub async fn validate_token(
&self,
pub_key_hash: &[u8],
address_metadata_hash: &[u8],
token: &str,
) -> Result<Vec<u8>, ValidationError<S::Error>> {
let url_safe_config = base64::Config::new(base64::CharacterSet::UrlSafe, false);
let outpoint_raw =
base64::decode_config(token, url_safe_config).map_err(ValidationError::Base64)?;
const PAYLOAD_LEN: usize = 32 + 4;
if outpoint_raw.len() != PAYLOAD_LEN {
return Err(ValidationError::TokenLength);
}
let tx_id = &outpoint_raw[..32];
let raw_transaction = self
.client
.get_raw_transaction(tx_id)
.await
.map_err(ValidationError::Node)?;
let transaction = Transaction::decode(&mut raw_transaction.as_slice())
.map_err(ValidationError::Transaction)?;
let vout_raw: [u8; 4] = outpoint_raw[32..36].try_into().unwrap();
let vout = u32::from_le_bytes(vout_raw);
let output = transaction
.outputs
.get(vout as usize)
.ok_or(ValidationError::OutputNotFound)?;
if !output.script.is_op_return() {
return Err(ValidationError::NotOpReturn);
}
let raw_script = output.script.as_bytes();
if raw_script.len() != 2 + COMMITMENT_LEN || raw_script[1] != COMMITMENT_LEN as u8 {
return Err(ValidationError::IncorrectLength);
}
let commitment = &raw_script[2..34];
let expected_commitment = construct_commitment(pub_key_hash, address_metadata_hash);
if expected_commitment != commitment {
return Err(ValidationError::Invalid);
}
Ok(outpoint_raw)
}
}