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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
use anyhow::{anyhow, Context, Result};
use rand_core::OsRng;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use url::Url;

use penumbra_asset::Value;
use penumbra_keys::{keys::AddressIndex, Address};
use penumbra_num::Amount;
use penumbra_proof_setup::all::{
    Phase1CeremonyContribution, Phase1RawCeremonyCRS, Phase2CeremonyContribution,
    Phase2RawCeremonyCRS,
};
use penumbra_proof_setup::single::log::Hashable;
use penumbra_proto::{
    penumbra::tools::summoning::v1::ceremony_coordinator_service_client::CeremonyCoordinatorServiceClient,
    tools::summoning::v1::{
        participate_request::{Identify, Msg as RequestMsg},
        participate_response::{Confirm, ContributeNow, Msg as ResponseMsg},
        ParticipateRequest, ParticipateResponse,
    },
    view::v1::GasPricesRequest,
};
use penumbra_view::Planner;

use crate::App;

fn max_message_size(phase: u8) -> usize {
    match phase {
        1 => 200 * 1024 * 1024,
        _ => 100 * 1024 * 1024,
    }
}

#[tracing::instrument(skip(app))]
async fn handle_bid(app: &mut App, to: Address, from: AddressIndex, bid: &str) -> Result<()> {
    let gas_prices = app
        .view
        .as_mut()
        .context("view service must be initialized")?
        .gas_prices(GasPricesRequest {})
        .await?
        .into_inner()
        .gas_prices
        .expect("gas prices must be available")
        .try_into()?;

    let value = bid.parse::<Value>()?;

    // If the bid is 0, skip creating a transaction. For instance, this allows reconnecting
    // without paying extra.
    if value.amount == 0u64.into() {
        return Ok(());
    }

    let mut planner = Planner::new(OsRng);
    planner.set_gas_prices(gas_prices);
    planner.output(value, to);
    let plan = planner
        .memo("E PLURIBUS UNUM".into())
        .memo_return_address(app.config.full_viewing_key.payment_address(from).0)
        .plan(
            app.view
                .as_mut()
                .context("view service must be initialized")?,
            from,
        )
        .await
        .context("can't build send transaction")?;
    app.build_and_submit_transaction(plan).await?;
    Ok(())
}

#[derive(Debug, clap::Subcommand)]
pub enum CeremonyCmd {
    /// Contribute to the ceremony
    Contribute {
        /// The phase of the summoning ceremony that's currently active. Must match that of the remote
        /// coordinator.
        #[clap(long, default_value = "2")]
        phase: u8,
        /// The URL for the public coordination server.
        #[clap(long, default_value = "https://summoning.penumbra.zone")]
        coordinator_url: Url,
        /// The Penumbra wallet address of the coordination server. Bids will be sent to this
        /// address, so the coordinator can compute the contributor's place in the queue.
        #[clap(
            long,
            default_value = "penumbra1qvqr8cvqyf4pwrl6svw9kj8eypf3fuunrcs83m30zxh57y2ytk94gygmtq5k82cjdq9y3mlaa3fwctwpdjr6fxnwuzrsy4ezm0u2tqpzw0sed82shzcr42sju55en26mavjnw4"
        )]
        coordinator_address: Address,
        /// Amount to spend during bid. Must be specified typed values, e.g. '50penumbra'.
        /// Only the 'penumbra' token is accepted for contributions. Bids are additive,
        /// so if you're disconnected, you can bid '0penumbra' and your previous bids
        /// will be still be counted when computing your place in the queue.
        #[clap(long)]
        bid: String,
    },
}

impl CeremonyCmd {
    #[tracing::instrument(skip(self, app))]
    pub async fn exec(&self, app: &mut App) -> Result<()> {
        match self {
            CeremonyCmd::Contribute {
                phase,
                coordinator_url,
                coordinator_address,
                bid,
            } => {
                println!("¸,ø¤º°` initiating summoning participation `°º¤ø,¸");

                let index = match *phase {
                    1 => AddressIndex {
                        account: 0,
                        randomizer: b"ceremnyaddr1"
                            .as_slice()
                            .try_into()
                            .expect("12 bytes long"),
                    },
                    2 => AddressIndex {
                        account: 0,
                        randomizer: b"ceremnyaddr2"
                            .as_slice()
                            .try_into()
                            .expect("12 bytes long"),
                    },
                    _ => anyhow::bail!("phase must be 1 or 2."),
                };
                let address = app.config.full_viewing_key.payment_address(index).0;

                println!(
                    "submitting bid {} for contribution slot from address {}",
                    bid, address
                );

                handle_bid(app, coordinator_address.clone(), index, bid).await?;

                println!("connecting to coordinator...");
                // After we bid, we need to wait a couple of seconds just for the transaction to be
                // picked up by the coordinator. Else, there is a race wherein the coordinator will kick the
                // client out of the queue because it doesn't see the transaction yet.
                tokio::time::sleep(std::time::Duration::from_secs(2)).await;

                let (req_tx, req_rx) = mpsc::channel::<ParticipateRequest>(10);
                tracing::debug!(?address, "participate request");
                req_tx
                    .send(ParticipateRequest {
                        msg: Some(RequestMsg::Identify(Identify {
                            address: Some(address.into()),
                        })),
                    })
                    .await?;
                let mut client =
                    CeremonyCoordinatorServiceClient::connect(coordinator_url.to_string())
                        .await?
                        .max_decoding_message_size(max_message_size(*phase))
                        .max_encoding_message_size(max_message_size(*phase));
                println!(
                    r#"connected to coordinator!
You may disconnect (CTRL+C) to increase your bid if you don't like your position in the queue.
Otherwise, please keep this window open.
"#
                );
                use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
                let progress_bar = ProgressBar::with_draw_target(1, ProgressDrawTarget::stdout())
                    .with_style(
                        ProgressStyle::default_bar()
                            .template("[{elapsed}] {bar:50.blue/cyan} position {pos} out of {len} connected summoners\t{msg}"),
                    );
                progress_bar.set_position(0);
                progress_bar.enable_steady_tick(1000);

                let mut response_rx = client
                    .participate(ReceiverStream::new(req_rx))
                    .await?
                    .into_inner();
                let unparsed_parent = loop {
                    match response_rx.message().await? {
                        None => {
                            progress_bar.abandon();
                            anyhow::bail!("Coordinator closed connection")
                        }
                        Some(ParticipateResponse {
                            msg: Some(ResponseMsg::Position(p)),
                        }) => {
                            tracing::debug!(?p);
                            let len = p.connected_participants;
                            // e.g. displaying 1 / 2 instead of 0 / 2
                            let pos = p.position + 1;
                            progress_bar.set_length(len as u64);
                            progress_bar.set_position(pos as u64);
                            progress_bar.set_message(format!(
                                "(your bid: {}, most recent slot bid: {})",
                                Amount::try_from(
                                    p.your_bid.ok_or(anyhow!("expected bid amount"))?
                                )?,
                                Amount::try_from(
                                    p.last_slot_bid.ok_or(anyhow!("expected top bid amount"))?
                                )?
                            ));
                            progress_bar.tick();
                        }
                        Some(ParticipateResponse {
                            msg:
                                Some(ResponseMsg::ContributeNow(ContributeNow {
                                    parent: Some(parent),
                                })),
                        }) => {
                            progress_bar.finish();
                            break parent;
                        }
                        m => {
                            progress_bar.abandon();
                            anyhow::bail!("Received unexpected  message from coordinator: {:?}", m)
                        }
                    }
                };
                println!("preparing contribution... (please keep this window open)");
                let (contribution, hash) = if *phase == 1 {
                    let parent = Phase1RawCeremonyCRS::unchecked_from_protobuf(unparsed_parent)?
                        .assume_valid();
                    let contribution = Phase1CeremonyContribution::make(&parent);
                    let hash = contribution.hash();
                    (contribution.try_into()?, hash)
                } else {
                    let parent = Phase2RawCeremonyCRS::unchecked_from_protobuf(unparsed_parent)?
                        .assume_valid();
                    let contribution = Phase2CeremonyContribution::make(&parent);
                    let hash = contribution.hash();
                    (contribution.try_into()?, hash)
                };
                println!("submitting contribution...");

                req_tx
                    .send(ParticipateRequest {
                        msg: Some(RequestMsg::Contribution(contribution)),
                    })
                    .await?;
                println!("coordinator is validating contribution...");
                match response_rx.message().await? {
                    None => anyhow::bail!("Coordinator closed connection"),
                    Some(ParticipateResponse {
                        msg: Some(ResponseMsg::Confirm(Confirm { slot })),
                    }) => {
                        println!("contribution confirmed at slot {slot}");
                        println!("thank you for your help summoning penumbra <3");
                        println!("here's your contribution receipt (save this to verify inclusion in the final transcript):\n{}", hex::encode_upper(hash.as_ref()));
                    }
                    m => {
                        anyhow::bail!("Received unexpected message from coordinator: {:?}", m)
                    }
                }

                Ok(())
            }
        }
    }
}