Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiple delegates. #3301

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5081,6 +5081,67 @@ mod tests {
}
}

#[test]
fn inscription_with_three_delegates_serves_the_first_available_one() {
for context in Context::configurations() {
context.mine_blocks(1);

let delegate_txid = context.rpc_server.broadcast_tx(TransactionTemplate {
inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
..Default::default()
});

context.mine_blocks(1);

let delegate_inscription_id_real = InscriptionId {
txid: delegate_txid,
index: 0,
};

let delegate_inscription_id_fake_a = InscriptionId {
txid: delegate_txid,
index: 1,
};
let delegate_inscription_id_fake_b = InscriptionId {
txid: delegate_txid,
index: 2,
};

let multi_delegate_inscription = Inscription {
content_type: Some("text/plain".into()),
body: Some("world".into()),
delegates: vec![
delegate_inscription_id_fake_a.value(),
delegate_inscription_id_real.value(),
delegate_inscription_id_fake_b.value(),
],
..Default::default()
};
let txid = context.rpc_server.broadcast_tx(TransactionTemplate {
inputs: &[(2, 0, 0, multi_delegate_inscription.to_witness())],
..Default::default()
});

context.mine_blocks(1);
let inscription_id = InscriptionId { txid, index: 0 };

let recovered_delegator = context
.index
.get_inscription_by_id(inscription_id)
.unwrap()
.unwrap();

assert_eq!(
recovered_delegator.delegates(),
vec![
delegate_inscription_id_fake_a,
delegate_inscription_id_real,
delegate_inscription_id_fake_b
]
);
}
}

#[test]
fn inscription_with_pointer() {
for context in Context::configurations() {
Expand Down
4 changes: 2 additions & 2 deletions src/inscriptions/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl From<RawEnvelope> for ParsedEnvelope {

let content_encoding = Tag::ContentEncoding.take(&mut fields);
let content_type = Tag::ContentType.take(&mut fields);
let delegate = Tag::Delegate.take(&mut fields);
let delegates = Tag::Delegate.take_array(&mut fields);
let metadata = Tag::Metadata.take(&mut fields);
let metaprotocol = Tag::Metaprotocol.take(&mut fields);
let parents = Tag::Parent.take_array(&mut fields);
Expand All @@ -72,7 +72,7 @@ impl From<RawEnvelope> for ParsedEnvelope {
}),
content_encoding,
content_type,
delegate,
delegates,
duplicate_field,
incomplete_field,
metadata,
Expand Down
31 changes: 18 additions & 13 deletions src/inscriptions/inscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct Inscription {
pub body: Option<Vec<u8>>,
pub content_encoding: Option<Vec<u8>>,
pub content_type: Option<Vec<u8>>,
pub delegate: Option<Vec<u8>>,
pub delegates: Vec<Vec<u8>>,
pub duplicate_field: bool,
pub incomplete_field: bool,
pub metadata: Option<Vec<u8>>,
Expand All @@ -40,7 +40,7 @@ impl Inscription {
pub(crate) fn from_file(
chain: Chain,
compress: bool,
delegate: Option<InscriptionId>,
delegates: Vec<InscriptionId>,
metadata: Option<Vec<u8>>,
metaprotocol: Option<String>,
parents: Vec<InscriptionId>,
Expand Down Expand Up @@ -101,7 +101,7 @@ impl Inscription {
body: Some(body),
content_encoding,
content_type: Some(content_type.into()),
delegate: delegate.map(|delegate| delegate.value()),
delegates: delegates.iter().map(|delegate| delegate.value()).collect(),
metadata,
metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()),
parents: parents.iter().map(|parent| parent.value()).collect(),
Expand Down Expand Up @@ -134,7 +134,7 @@ impl Inscription {
Tag::ContentEncoding.append(&mut builder, &self.content_encoding);
Tag::Metaprotocol.append(&mut builder, &self.metaprotocol);
Tag::Parent.append_array(&mut builder, &self.parents);
Tag::Delegate.append(&mut builder, &self.delegate);
Tag::Delegate.append_array(&mut builder, &self.delegates);
Tag::Pointer.append(&mut builder, &self.pointer);
Tag::Metadata.append(&mut builder, &self.metadata);
Tag::Rune.append(&mut builder, &self.rune);
Expand Down Expand Up @@ -239,8 +239,12 @@ impl Inscription {
HeaderValue::from_str(str::from_utf8(self.content_encoding.as_ref()?).unwrap_or_default()).ok()
}

pub(crate) fn delegate(&self) -> Option<InscriptionId> {
Self::inscription_id_field(self.delegate.as_deref())
pub(crate) fn delegates(&self) -> Vec<InscriptionId> {
self
.delegates
.iter()
.filter_map(|delegate| Self::inscription_id_field(Some(delegate)))
.collect()
}

pub(crate) fn metadata(&self) -> Option<Value> {
Expand Down Expand Up @@ -488,14 +492,15 @@ mod tests {
fn inscription_delegate_txid_is_deserialized_correctly() {
assert_eq!(
Inscription {
delegate: Some(vec![
delegates: vec![vec![
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
0x1e, 0x1f,
]),
]],
..Default::default()
}
.delegate()
.delegates()
.first()
.unwrap()
.txid,
"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"
Expand Down Expand Up @@ -777,7 +782,7 @@ mod tests {
let inscription = Inscription::from_file(
Chain::Mainnet,
false,
None,
Vec::new(),
None,
None,
Vec::new(),
Expand All @@ -792,7 +797,7 @@ mod tests {
let inscription = Inscription::from_file(
Chain::Mainnet,
false,
None,
Vec::new(),
None,
None,
Vec::new(),
Expand All @@ -807,7 +812,7 @@ mod tests {
let inscription = Inscription::from_file(
Chain::Mainnet,
false,
None,
Vec::new(),
None,
None,
Vec::new(),
Expand All @@ -822,7 +827,7 @@ mod tests {
let inscription = Inscription::from_file(
Chain::Mainnet,
false,
None,
Vec::new(),
None,
None,
Vec::new(),
Expand Down
26 changes: 15 additions & 11 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1341,11 +1341,14 @@ impl Server {
};
};

if let Some(delegate) = inscription.delegate() {
inscription = index
.get_inscription_by_id(delegate)?
.ok_or_not_found(|| format!("delegate {inscription_id}"))?
}
// return the first inscription where the delegate exists
// probably worth considering some sort of limitation to mitigate "DoSability"
let inscription_override = inscription
.delegates()
.iter()
.find_map(|delegate| index.get_inscription_by_id(*delegate).unwrap_or(None));
Copy link
Contributor Author

@arik-so arik-so Mar 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really worried about this being a DoS vector. It would be fairly trivial to inscribe something with a couple thousand non-existent delegates. Any thoughts on whether it's a) a non-issue, b) makes the multi-delegate approach altogether unviable, or c) there are some mitigating strategies? @raphjaph

Copy link

@cryptocj cryptocj Mar 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a limit number to avoid the Dos concern?
And extract a function to remove the repeat code below.

fn find_inscription<'a>(delegates: &'a [usize], index: &'a Index, max_length: usize) -> Option<&'a Inscription> {
    delegates
        .iter()
        .take(max_length)
        .find_map(|delegate| index.get_inscription_by_id(*delegate).unwrap_or(None))
}

// Usage
let inscription_override = find_inscription(inscription.delegates(), &index, max_length);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, the question is just what the limit should be. That's why I think @casey or @raphjaph should weigh in.


inscription = inscription_override.unwrap_or(inscription);

Ok(
Self::content_response(inscription, accept_encoding, &server_config)?
Expand Down Expand Up @@ -1441,11 +1444,12 @@ impl Server {
.get_inscription_by_id(inscription_id)?
.ok_or_not_found(|| format!("inscription {inscription_id}"))?;

if let Some(delegate) = inscription.delegate() {
inscription = index
.get_inscription_by_id(delegate)?
.ok_or_not_found(|| format!("delegate {inscription_id}"))?
}
let inscription_override = inscription
.delegates()
.iter()
.find_map(|delegate| index.get_inscription_by_id(*delegate).unwrap_or(None));

inscription = inscription_override.unwrap_or(inscription);

let media = inscription.media();

Expand Down Expand Up @@ -5893,7 +5897,7 @@ next
server.mine_blocks(1);

let inscription = Inscription {
delegate: Some(delegate.value()),
delegates: vec![delegate.value()],
..Default::default()
};

Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/wallet/inscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ impl Inscribe {
inscriptions = vec![Inscription::from_file(
chain,
self.compress,
self.delegate,
self.delegate.into_iter().collect(),
metadata,
self.metaprotocol,
self.parent.into_iter().collect(),
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/inscribe/batchfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ impl Batchfile {
inscriptions.push(Inscription::from_file(
wallet.chain(),
compress,
entry.delegate,
entry.delegate.into_iter().collect(),
entry.metadata()?,
entry.metaprotocol.clone(),
self.parent.into_iter().collect(),
Expand Down
4 changes: 2 additions & 2 deletions templates/inscription.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ <h1>Inscription {{ self.number }}</h1>
<dt>metaprotocol</dt>
<dd>{{ metaprotocol }}</dd>
%% }
%% if self.inscription.content_length().is_some() || self.inscription.delegate().is_some() {
%% if let Some(delegate) = self.inscription.delegate() {
%% if self.inscription.content_length().is_some() || self.inscription.delegates().first().is_some() {
%% if let Some(delegate) = self.inscription.delegates().first() {
<dt>delegate</dt>
<dd><a href=/inscription/{{ delegate }}>{{ delegate }}</a></dd>
%% }
Expand Down
Loading