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

feat: add fetch_api_boundary_nodes API to ic-agent #509

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

* Added `Agent::fetch_api_boundary_nodes` for looking up API boundary nodes in the state tree.
* Timestamps are now being checked in `Agent::verify` and `Agent::verify_for_subnet`. If you were using it with old certificates, increase the expiry timeout to continue to verify them.
* Added node metrics, ECDSA, and Bitcoin functions to `MgmtMethod`. Most do not have wrappers in `ManagementCanister` because only canisters can call these functions.
* Added `FetchCanisterLogs` function to `MgmtMethod` and a corresponding wrapper to `ManagementCanister`.
Expand Down
28 changes: 28 additions & 0 deletions ic-agent/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ use std::{
time::Duration,
};

use crate::agent::response_authentication::lookup_api_boundary_nodes;

const IC_STATE_ROOT_DOMAIN_SEPARATOR: &[u8; 14] = b"\x0Dic-state-root";

const IC_ROOT_KEY: &[u8; 133] = b"\x30\x81\x82\x30\x1d\x06\x0d\x2b\x06\x01\x04\x01\x82\xdc\x7c\x05\x03\x01\x02\x01\x06\x0c\x2b\x06\x01\x04\x01\x82\xdc\x7c\x05\x03\x02\x01\x03\x61\x00\x81\x4c\x0e\x6e\xc7\x1f\xab\x58\x3b\x08\xbd\x81\x37\x3c\x25\x5c\x3c\x37\x1b\x2e\x84\x86\x3c\x98\xa4\xf1\xe0\x8b\x74\x23\x5d\x14\xfb\x5d\x9c\x0c\xd5\x46\xd9\x68\x5f\x91\x3a\x0c\x0b\x2c\xc5\x34\x15\x83\xbf\x4b\x43\x92\xe4\x67\xdb\x96\xd6\x5b\x9b\xb4\xcb\x71\x71\x12\xf8\x47\x2e\x0d\x5a\x4d\x14\x50\x5f\xfd\x74\x84\xb0\x12\x91\x09\x1c\x5f\x87\xb9\x88\x83\x46\x3f\x98\x09\x1a\x0b\xaa\xae";
Expand Down Expand Up @@ -1121,6 +1123,21 @@ impl Agent {
}
}

/// Retrieve all existing API boundary nodes from the state tree.
pub async fn fetch_api_boundary_nodes(
&self,
effective_canister_id: Principal,
) -> Result<Vec<ApiBoundaryNode>, AgentError> {
let certificate = self
.read_state_raw(
vec![vec!["api_boundary_nodes".into()]],
effective_canister_id,
)
.await?;
let api_boundary_nodes = lookup_api_boundary_nodes(certificate)?;
Ok(api_boundary_nodes)
}

async fn fetch_subnet_by_canister(
&self,
canister: &Principal,
Expand Down Expand Up @@ -1446,6 +1463,17 @@ pub(crate) struct Subnet {
canister_ranges: RangeInclusiveSet<Principal, PrincipalStep>,
}

/// API boundary node, which routes /api calls to IC replica nodes.
#[derive(Debug)]
pub struct ApiBoundaryNode {
/// Domain name
pub domain: String,
/// IPv6 address in the hexadecimal notation with colons.
pub ipv6_address: String,
/// IPv4 address in the dotted-decimal notation.
pub ipv4_address: Option<String>,
}

/// A Query Request Builder.
///
/// This makes it easier to do query calls without actually passing all arguments.
Expand Down
50 changes: 48 additions & 2 deletions ic-agent/src/agent/response_authentication.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::agent::{RejectCode, RejectResponse, RequestStatusResponse};
use crate::agent::{ApiBoundaryNode, RejectCode, RejectResponse, RequestStatusResponse};
use crate::{export::Principal, AgentError, RequestId};
use ic_certification::hash_tree::{HashTree, SubtreeLookupResult};
use ic_certification::{certificate::Certificate, hash_tree::Label, LookupResult};
use ic_transport_types::{ReplyResponse, SubnetMetrics};
use rangemap::RangeInclusiveSet;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::str::from_utf8;

use super::Subnet;
Expand Down Expand Up @@ -213,6 +213,52 @@ pub(crate) fn lookup_subnet<Storage: AsRef<[u8]> + Clone>(
Ok((subnet_id, subnet))
}

pub(crate) fn lookup_api_boundary_nodes<Storage: AsRef<[u8]> + Clone>(
certificate: Certificate<Storage>,
) -> Result<Vec<ApiBoundaryNode>, AgentError> {
// API boundary nodes paths in the state tree, as defined in the spec (https://internetcomputer.org/docs/current/references/ic-interface-spec#state-tree-api-bn).
let api_bn_path = "api_boundary_nodes".as_bytes();
let domain_path = "domain".as_bytes();
let ipv4_path = "ipv4_address".as_bytes();
let ipv6_path = "ipv6_address".as_bytes();

let api_bn_tree = lookup_tree(&certificate.tree, [api_bn_path])?;

let mut api_bns = Vec::<ApiBoundaryNode>::new();
let paths = api_bn_tree.list_paths();
let node_ids: HashSet<&[u8]> = paths.iter().map(|path| path[0].as_bytes()).collect();

for node_id in node_ids {
let domain =
String::from_utf8(lookup_value(&api_bn_tree, [node_id, domain_path])?.to_vec())
.map_err(|err| AgentError::Utf8ReadError(err.utf8_error()))?;

let ipv6_address =
String::from_utf8(lookup_value(&api_bn_tree, [node_id, ipv6_path])?.to_vec())
.map_err(|err| AgentError::Utf8ReadError(err.utf8_error()))?;

let ipv4_address = match lookup_value(&api_bn_tree, [node_id, ipv4_path]) {
Ok(ipv4) => Some(
String::from_utf8(ipv4.to_vec())
.map_err(|err| AgentError::Utf8ReadError(err.utf8_error()))?,
),
// By convention an absent path `/api_boundary_nodes/<node_id>/ipv4_address` in the state tree signifies that ipv4 is None.
Err(AgentError::LookupPathAbsent(_)) => None,
Err(err) => return Err(err),
};

let api_bn = ApiBoundaryNode {
domain,
ipv6_address,
ipv4_address,
};

api_bns.push(api_bn);
}

Ok(api_bns)
}

/// The path to [`lookup_value`]
pub trait LookupPath {
type Item: AsRef<[u8]>;
Expand Down
Loading