Skip to content

Commit fa10763

Browse files
authored
feat(rpc)!: Expose the end of support height (#11097)
2 parents 25d0c1b + 10f9f80 commit fa10763

9 files changed

Lines changed: 403 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org).
77

88
## [Unreleased]
99

10+
### Breaking Changes
11+
12+
- New `getdeprecationinfo` RPC returning the block height and estimated time at which this
13+
release will halt for end of support, in zcashd's `end_of_service` format. The `end_of_service`
14+
object is only present on Mainnet, where end of support is enforced
15+
([#11097](https://github.com/ZcashFoundation/zebra/pull/11097)).
16+
1017
### Added
1118

1219
- Added `seeder.zec.rocks` and `seeder.testnet.zec.rocks` as default DNS seeders

zebra-rpc/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Breaking Changes
11+
12+
- The `Rpc` trait has a new required `get_deprecation_info` method. Downstream implementers of
13+
the trait must add it; callers of `RpcImpl` are unaffected
14+
([#11097](https://github.com/ZcashFoundation/zebra/pull/11097)).
15+
16+
### Added
17+
18+
- New `getdeprecationinfo` RPC method and `GetDeprecationInfoResponse` type. The reported end of
19+
support height is set with `RpcImpl::with_end_of_support_height`; without it the response omits
20+
the `end_of_service` object ([#11097](https://github.com/ZcashFoundation/zebra/pull/11097)).
21+
1022
### Changed
1123

1224
- The indexer gRPC server now bounds concurrent HTTP/2 streams per connection (20)

zebra-rpc/src/client.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ pub use crate::methods::{
3737
validate_address::ValidateAddressResponse,
3838
z_validate_address::{ZValidateAddressResponse, ZValidateAddressType},
3939
},
40-
AddressStrings, BlockHeaderObject, BlockObject, GetAddressBalanceRequest,
40+
AddressStrings, BlockHeaderObject, BlockObject, EndOfService, GetAddressBalanceRequest,
4141
GetAddressBalanceResponse, GetAddressTxIdsRequest, GetAddressUtxosResponse,
4242
GetAddressUtxosResponseObject, GetBlockHashResponse, GetBlockHeaderResponse,
4343
GetBlockHeightAndHashResponse, GetBlockResponse, GetBlockTransaction, GetBlockTrees,
44-
GetBlockchainInfoResponse, GetInfoResponse, GetRawTransactionResponse, Hash,
45-
SendRawTransactionResponse, Utxo,
44+
GetBlockchainInfoResponse, GetDeprecationInfoResponse, GetInfoResponse,
45+
GetRawTransactionResponse, Hash, SendRawTransactionResponse, Utxo,
4646
};
4747

4848
/// Constants needed by clients of Zebra's RPC server

zebra-rpc/src/methods.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,28 @@ pub trait Rpc {
194194
#[method(name = "getinfo")]
195195
async fn get_info(&self) -> Result<GetInfoResponse>;
196196

197+
/// Returns end of support information for this node release, as a
198+
/// [`GetDeprecationInfoResponse`] JSON struct.
199+
///
200+
/// zcashd reference: [`getdeprecationinfo`](https://zcash.github.io/rpc/getdeprecationinfo.html)
201+
/// method: post
202+
/// tags: network
203+
///
204+
/// # Notes
205+
///
206+
/// As in zcashd, the `end_of_service` object is only present on Mainnet, where end of
207+
/// support is enforced. Zebra reports the estimated last height this release supports in
208+
/// `end_of_service.block_height`; the node halts when the tip goes past it.
209+
///
210+
/// Some fields from the zcashd reference are missing from Zebra's response: `version` and
211+
/// `subversion` are available from `getinfo`, `deprecationheight` is deprecated in zcashd,
212+
/// and Zebra does not have zcashd's feature deprecation framework.
213+
///
214+
/// The estimate assumes the node is synced to the network tip; during initial sync it is
215+
/// significantly overestimated.
216+
#[method(name = "getdeprecationinfo")]
217+
async fn get_deprecation_info(&self) -> Result<GetDeprecationInfoResponse>;
218+
197219
/// Returns blockchain state information, as a [`GetBlockchainInfoResponse`] JSON struct.
198220
///
199221
/// zcashd reference: [`getblockchaininfo`](https://zcash.github.io/rpc/getblockchaininfo.html)
@@ -836,6 +858,10 @@ where
836858
/// no matter what the estimated height or local clock is.
837859
debug_force_finished_sync: bool,
838860

861+
/// The estimated last height this node release supports before its end of support halt,
862+
/// if end of support is enforced on `network`. Reported by the `getdeprecationinfo` RPC.
863+
end_of_support_height: Option<Height>,
864+
839865
// Services
840866
//
841867
/// A handle to the mempool service.
@@ -950,6 +976,7 @@ where
950976
user_agent,
951977
network: network.clone(),
952978
debug_force_finished_sync,
979+
end_of_support_height: None,
953980
mempool: mempool.clone(),
954981
state: state.clone(),
955982
read_state: read_state.clone(),
@@ -974,6 +1001,14 @@ where
9741001
pub fn network(&self) -> &Network {
9751002
&self.network
9761003
}
1004+
1005+
/// Sets the estimated end of support height reported by the `getdeprecationinfo` RPC.
1006+
///
1007+
/// When unset, or set to `None`, `getdeprecationinfo` omits the `end_of_service` object.
1008+
pub fn with_end_of_support_height(mut self, end_of_support_height: Option<Height>) -> Self {
1009+
self.end_of_support_height = end_of_support_height;
1010+
self
1011+
}
9771012
}
9781013

9791014
#[async_trait]
@@ -1039,6 +1074,44 @@ where
10391074
Ok(response)
10401075
}
10411076

1077+
async fn get_deprecation_info(&self) -> Result<GetDeprecationInfoResponse> {
1078+
let end_of_service = self
1079+
.end_of_support_height
1080+
// End of support is only enforced on Mainnet, and the zcashd-compatible response
1081+
// omits `end_of_service` on other networks, even if a height was configured.
1082+
.filter(|_| self.network == Network::Mainnet)
1083+
.map(|end_of_support_height| {
1084+
// Estimate the halt time from the current tip and target block spacing. If the
1085+
// tip is not available yet, fall back to the highest compiled-in checkpoint,
1086+
// which is close to the network tip at release time.
1087+
let tip_height = self
1088+
.latest_chain_tip
1089+
.best_tip_height()
1090+
.unwrap_or_else(|| self.network.checkpoint_list().max_height());
1091+
// Use the spacing at the end of support height: it is always post-Blossom, so
1092+
// this stays correct even when the tip is missing or before Blossom.
1093+
let target_block_spacing =
1094+
NetworkUpgrade::target_spacing_for_height(&self.network, end_of_support_height);
1095+
// If the tip is already past the end of support height, the estimate is in the
1096+
// past, but never negative.
1097+
let remaining_blocks = i64::from(end_of_support_height.0) - i64::from(tip_height.0);
1098+
let estimated_time = Utc::now()
1099+
.timestamp()
1100+
.saturating_add(
1101+
remaining_blocks.saturating_mul(target_block_spacing.num_seconds()),
1102+
)
1103+
.saturating_sub(END_OF_SERVICE_ESTIMATE_SAFETY_MARGIN)
1104+
.max(0);
1105+
1106+
EndOfService {
1107+
block_height: end_of_support_height.0,
1108+
estimated_time,
1109+
}
1110+
});
1111+
1112+
Ok(GetDeprecationInfoResponse { end_of_service })
1113+
}
1114+
10421115
#[allow(clippy::unwrap_in_result)]
10431116
async fn get_blockchain_info(&self) -> Result<GetBlockchainInfoResponse> {
10441117
let debug_force_finished_sync = self.debug_force_finished_sync;
@@ -3508,6 +3581,41 @@ impl GetInfoResponse {
35083581
}
35093582
}
35103583

3584+
/// The number of seconds subtracted from the `end_of_service.estimated_time` reported by
3585+
/// `getdeprecationinfo`.
3586+
///
3587+
/// Block times vary, so the halt can happen earlier than a spacing-based estimate. Reporting the
3588+
/// estimate a day early gives consumers time to act before the actual halt.
3589+
const END_OF_SERVICE_ESTIMATE_SAFETY_MARGIN: i64 = 24 * 60 * 60;
3590+
3591+
/// Response to a `getdeprecationinfo` RPC request.
3592+
///
3593+
/// See the notes for the [`Rpc::get_deprecation_info` method].
3594+
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
3595+
pub struct GetDeprecationInfoResponse {
3596+
/// End of service information for this node release. Only present on Mainnet, where end of
3597+
/// support is enforced.
3598+
#[serde(skip_serializing_if = "Option::is_none")]
3599+
end_of_service: Option<EndOfService>,
3600+
}
3601+
3602+
/// The `end_of_service` object in a [`GetDeprecationInfoResponse`].
3603+
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Getters, new)]
3604+
pub struct EndOfService {
3605+
/// The estimated last height this server version supports, matching zcashd's threshold
3606+
/// semantics. The node halts when the chain tip goes past this height.
3607+
#[getter(copy)]
3608+
block_height: u32,
3609+
3610+
/// The approximate time of the end of support halt, in seconds since epoch, estimated from
3611+
/// the current chain tip height and the target block spacing.
3612+
///
3613+
/// Reported 24 hours earlier than the spacing-based estimate, so consumers are warned early
3614+
/// rather than late when block times vary.
3615+
#[getter(copy)]
3616+
estimated_time: i64,
3617+
}
3618+
35113619
/// Type alias for the array of `GetBlockchainInfoBalance` objects
35123620
pub type BlockchainValuePoolBalances = [GetBlockchainInfoBalance; 6];
35133621

zebra-rpc/src/methods/tests/vectors.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,189 @@ async fn rpc_getinfo() {
116116
assert!(rpc_tx_queue_task_result.is_none());
117117
}
118118

119+
#[tokio::test(flavor = "multi_thread")]
120+
async fn rpc_getdeprecationinfo() {
121+
let _init_guard = zebra_test::init();
122+
123+
let mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
124+
let state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
125+
let read_state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
126+
127+
let (_tx, rx) = tokio::sync::watch::channel(None);
128+
let (rpc, _rpc_tx_queue) = RpcImpl::new(
129+
Mainnet,
130+
Default::default(),
131+
Default::default(),
132+
"0.0.1",
133+
"RPC test",
134+
Buffer::new(mempool.clone(), 1),
135+
Buffer::new(state.clone(), 1),
136+
Buffer::new(read_state.clone(), 1),
137+
MockService::build().for_unit_tests(),
138+
MockSyncStatus::default(),
139+
NoChainTip,
140+
MockAddressBookPeers::default(),
141+
rx,
142+
None,
143+
);
144+
145+
// Without a configured end of support height, `end_of_service` is not present.
146+
let deprecation_info = rpc
147+
.get_deprecation_info()
148+
.await
149+
.expect("getdeprecationinfo should succeed");
150+
assert_eq!(deprecation_info.end_of_service, None);
151+
152+
let end_of_support_height = 3_546_440;
153+
let rpc = rpc.with_end_of_support_height(Some(Height(end_of_support_height)));
154+
let end_of_service = rpc
155+
.get_deprecation_info()
156+
.await
157+
.expect("getdeprecationinfo should succeed")
158+
.end_of_service
159+
.expect("end_of_service should be present when an end of support height is set");
160+
161+
assert_eq!(end_of_service.block_height, end_of_support_height);
162+
// This node has no tip, so the estimate counts the blocks remaining after the highest
163+
// compiled-in checkpoint, not after genesis.
164+
let checkpoint_height = Mainnet.checkpoint_list().max_height();
165+
let remaining_blocks = i64::from(end_of_support_height - checkpoint_height.0);
166+
let expected_offset = remaining_blocks * 75 - 24 * 60 * 60;
167+
assert!(end_of_service.estimated_time > Utc::now().timestamp());
168+
assert!(end_of_service.estimated_time <= Utc::now().timestamp() + expected_offset);
169+
}
170+
171+
#[tokio::test(flavor = "multi_thread")]
172+
async fn rpc_getdeprecationinfo_estimates_time_from_tip_with_safety_margin() {
173+
let _init_guard = zebra_test::init();
174+
175+
let mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
176+
let state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
177+
let read_state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
178+
179+
let (latest_chain_tip, latest_chain_tip_sender) = MockChainTip::new();
180+
let tip_height = Height(3_000_000);
181+
latest_chain_tip_sender.send_best_tip_height(tip_height);
182+
183+
let (_tx, rx) = tokio::sync::watch::channel(None);
184+
let (rpc, _rpc_tx_queue) = RpcImpl::new(
185+
Mainnet,
186+
Default::default(),
187+
Default::default(),
188+
"0.0.1",
189+
"RPC test",
190+
Buffer::new(mempool.clone(), 1),
191+
Buffer::new(state.clone(), 1),
192+
Buffer::new(read_state.clone(), 1),
193+
MockService::build().for_unit_tests(),
194+
MockSyncStatus::default(),
195+
latest_chain_tip,
196+
MockAddressBookPeers::default(),
197+
rx,
198+
None,
199+
);
200+
201+
let end_of_support_height = Height(3_546_440);
202+
let rpc = rpc.with_end_of_support_height(Some(end_of_support_height));
203+
204+
// Both heights are after Blossom, so every remaining block is expected to take 75 seconds,
205+
// and the estimate is reported 24 hours early.
206+
let remaining_blocks = i64::from(end_of_support_height.0 - tip_height.0);
207+
let expected_offset = remaining_blocks * 75 - 24 * 60 * 60;
208+
209+
let before = Utc::now().timestamp();
210+
let end_of_service = rpc
211+
.get_deprecation_info()
212+
.await
213+
.expect("getdeprecationinfo should succeed")
214+
.end_of_service
215+
.expect("end_of_service should be present when an end of support height is set");
216+
let after = Utc::now().timestamp();
217+
218+
assert_eq!(end_of_service.block_height, end_of_support_height.0);
219+
assert!(end_of_service.estimated_time >= before + expected_offset);
220+
assert!(end_of_service.estimated_time <= after + expected_offset);
221+
}
222+
223+
#[tokio::test(flavor = "multi_thread")]
224+
async fn rpc_getdeprecationinfo_omits_end_of_service_off_mainnet() {
225+
let _init_guard = zebra_test::init();
226+
227+
let mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
228+
let state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
229+
let read_state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
230+
231+
let (_tx, rx) = tokio::sync::watch::channel(None);
232+
let (rpc, _rpc_tx_queue) = RpcImpl::new(
233+
Network::new_default_testnet(),
234+
Default::default(),
235+
Default::default(),
236+
"0.0.1",
237+
"RPC test",
238+
Buffer::new(mempool.clone(), 1),
239+
Buffer::new(state.clone(), 1),
240+
Buffer::new(read_state.clone(), 1),
241+
MockService::build().for_unit_tests(),
242+
MockSyncStatus::default(),
243+
NoChainTip,
244+
MockAddressBookPeers::default(),
245+
rx,
246+
None,
247+
);
248+
249+
// Even if an end of support height is configured, `end_of_service` is only reported on
250+
// Mainnet, matching zcashd and the RPC documentation.
251+
let rpc = rpc.with_end_of_support_height(Some(Height(100)));
252+
let deprecation_info = rpc
253+
.get_deprecation_info()
254+
.await
255+
.expect("getdeprecationinfo should succeed");
256+
assert_eq!(deprecation_info.end_of_service, None);
257+
}
258+
259+
#[tokio::test(flavor = "multi_thread")]
260+
async fn rpc_getdeprecationinfo_estimated_time_is_never_negative() {
261+
let _init_guard = zebra_test::init();
262+
263+
let mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
264+
let state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
265+
let read_state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
266+
267+
// A tip so far past the end of support height that an unclamped estimate would be negative.
268+
let (latest_chain_tip, latest_chain_tip_sender) = MockChainTip::new();
269+
latest_chain_tip_sender.send_best_tip_height(Height::MAX);
270+
271+
let (_tx, rx) = tokio::sync::watch::channel(None);
272+
let (rpc, _rpc_tx_queue) = RpcImpl::new(
273+
Mainnet,
274+
Default::default(),
275+
Default::default(),
276+
"0.0.1",
277+
"RPC test",
278+
Buffer::new(mempool.clone(), 1),
279+
Buffer::new(state.clone(), 1),
280+
Buffer::new(read_state.clone(), 1),
281+
MockService::build().for_unit_tests(),
282+
MockSyncStatus::default(),
283+
latest_chain_tip,
284+
MockAddressBookPeers::default(),
285+
rx,
286+
None,
287+
);
288+
let rpc = rpc.with_end_of_support_height(Some(Height(1)));
289+
290+
let end_of_service = rpc
291+
.get_deprecation_info()
292+
.await
293+
.expect("getdeprecationinfo should succeed")
294+
.end_of_service
295+
.expect("end_of_service should be present when an end of support height is set");
296+
297+
assert_eq!(end_of_service.block_height, 1);
298+
// The estimate is clamped to zero instead of going negative.
299+
assert_eq!(end_of_service.estimated_time, 0);
300+
}
301+
119302
// Helper function that returns the nonce, final sapling root and
120303
// block commitments of a given Block.
121304
async fn get_block_data(

0 commit comments

Comments
 (0)