use alloc::vec::Vec;
use super::key::Key;
use crate::abi::{
collect_hash_words, encode_bytes, encode_dynamic_array, encode_tuple, AbiReader, Field,
};
use crate::hash::{
base_w_digit, derive32, hash_node, hash_packed, hypertree_address_word, word32,
wots_address_base, wots_chain_address_word, wots_digest_bytes,
};
use crate::profiles::{HYPERTREE_HEIGHT, NUM_HYPERTREE_LAYERS, NUM_WOTS_CHAINS, WOTS_CHAIN_LEN};
use crate::wots_c::{wots_chain_walk, ChainWalk, Signature, TARGET_SUM, WOTS_C_MAX_GRIND_COUNTER};
use crate::HASH_LEN;
use zeroize::Zeroizing;
const HYPERTREE_SUBTREE_HEIGHT: usize =
(HYPERTREE_HEIGHT as usize) / (NUM_HYPERTREE_LAYERS as usize);
const _: () = {
assert!(
HYPERTREE_SUBTREE_HEIGHT > 0 && HYPERTREE_SUBTREE_HEIGHT < u32::BITS as usize,
"HYPERTREE_HEIGHT/NUM_HYPERTREE_LAYERS must yield a subtree height in 1..32"
);
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayerSignature {
pub wots_c_pk_hash: [u8; HASH_LEN],
pub wots_c_signature: Signature,
pub auth_path: Vec<[u8; HASH_LEN]>,
}
impl LayerSignature {
pub fn to_bytes(&self) -> Vec<u8> {
encode_tuple(alloc::vec![
Field::Dynamic(encode_bytes(&self.wots_c_pk_hash)),
Field::Dynamic(self.wots_c_signature.to_bytes()),
Field::Dynamic(encode_dynamic_array(
self.auth_path
.iter()
.map(|node| encode_bytes(node))
.collect(),
)),
])
}
pub(crate) fn decode(reader: &AbiReader, base: usize) -> Option<Self> {
let wots_head = base.checked_add(32)?;
let wots_start = reader.decode_offset(base, wots_head)?;
Some(Self {
wots_c_pk_hash: reader.decode_bytes32_field(base, base)?,
wots_c_signature: Signature::decode(reader, wots_start)?,
auth_path: collect_hash_words(reader.decode_array_bytes(
base,
base.checked_add(64)?,
HYPERTREE_SUBTREE_HEIGHT,
)?)?,
})
}
pub fn from_bytes(data: &[u8]) -> Option<Self> {
let reader = AbiReader::new(data);
let decoded = Self::decode(&reader, 0)?;
reader.finish()?;
Some(decoded)
}
}
impl TryFrom<&[u8]> for LayerSignature {
type Error = ();
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::from_bytes(value).ok_or(())
}
}
#[derive(Clone, Copy)]
pub(crate) struct HypertreeSeed {
pub tree_index: u64,
pub leaf_index: u32,
}
pub(crate) fn verify_hypertree(
pk_seed: &[u8; HASH_LEN],
expected_hypertree_root: &[u8; HASH_LEN],
fors_root: [u8; HASH_LEN],
seed: HypertreeSeed,
layers: &[LayerSignature],
) -> bool {
if layers.len() != NUM_HYPERTREE_LAYERS as usize {
return false;
}
let subtree_height = u32::from(HYPERTREE_HEIGHT / NUM_HYPERTREE_LAYERS);
if subtree_height == 0 || subtree_height >= u32::BITS {
return false;
}
let leaf_count = 1u32 << subtree_height;
let leaf_mask = (1u64 << subtree_height) - 1;
let mut current_root = fors_root;
let mut expected_tree_index = seed.tree_index;
let mut expected_leaf_index = seed.leaf_index;
for (layer_index, layer_signature) in layers.iter().enumerate() {
if expected_leaf_index >= leaf_count
|| layer_signature.auth_path.len() != subtree_height as usize
{
return false;
}
let coords = WotsKeypair {
layer: layer_index as u32,
tree: expected_tree_index,
keypair: expected_leaf_index,
};
if !verify_wots_c32(
pk_seed,
coords,
&layer_signature.wots_c_pk_hash,
current_root,
&layer_signature.wots_c_signature,
) {
return false;
}
let Some(next_root) = hypertree_root_from_path32(
subtree_height,
pk_seed,
HypertreePath {
layer: layer_index as u32,
tree_index: expected_tree_index,
leaf_index: expected_leaf_index,
},
layer_signature.wots_c_pk_hash,
&layer_signature.auth_path,
) else {
return false;
};
current_root = next_root;
expected_leaf_index = (expected_tree_index & leaf_mask) as u32;
expected_tree_index >>= subtree_height;
}
expected_tree_index == 0 && *expected_hypertree_root == current_root
}
pub(crate) fn stateless_wots_message_digest(
pk_seed: &[u8; HASH_LEN],
expected_pk_hash: &[u8; HASH_LEN],
randomizer: &[u8; HASH_LEN],
counter: u32,
message: &[u8; HASH_LEN],
) -> [u8; HASH_LEN] {
hash_packed(&[
b"wots-c-msg".as_ref(),
pk_seed.as_ref(),
expected_pk_hash.as_ref(),
randomizer.as_ref(),
counter.to_be_bytes().as_ref(),
message.as_ref(),
])
}
pub(crate) fn stateless_wots_public_key_hash(
pk_seed: &[u8; HASH_LEN],
endpoints: &[[u8; HASH_LEN]],
) -> [u8; HASH_LEN] {
const MAX_PARTS: usize = NUM_WOTS_CHAINS as usize + 2;
let mut parts: [&[u8]; MAX_PARTS] = [&[]; MAX_PARTS];
parts[0] = b"wots-c-pk";
parts[1] = pk_seed.as_ref();
let used = 2 + endpoints.len().min(NUM_WOTS_CHAINS as usize);
for (part, endpoint) in parts[2..used].iter_mut().zip(endpoints) {
*part = endpoint.as_ref();
}
hash_node(&parts[..used])
}
fn verify_wots_c32(
pk_seed: &[u8; HASH_LEN],
coords: WotsKeypair,
expected_pk_hash: &[u8; HASH_LEN],
message: [u8; HASH_LEN],
signature: &Signature,
) -> bool {
let chain_count = NUM_WOTS_CHAINS as usize;
if signature.chains.len() != chain_count || wots_digest_bytes() > HASH_LEN {
return false;
}
let digest = stateless_wots_message_digest(
pk_seed,
expected_pk_hash,
&signature.randomizer,
signature.counter,
&message,
);
let address_base = wots_address_base(coords.layer, coords.tree, coords.keypair);
let mut digit_sum = 0u32;
let mut digits = [0u32; NUM_WOTS_CHAINS as usize];
for (chain_index, digit_slot) in digits.iter_mut().enumerate() {
let digit = base_w_digit(WOTS_CHAIN_LEN, &digest, chain_index);
let Some(next_sum) = digit_sum.checked_add(digit) else {
return false;
};
digit_sum = next_sum;
*digit_slot = digit;
}
if digit_sum != TARGET_SUM {
return false;
}
let mut segments = crate::buf::node_buf::<{ NUM_WOTS_CHAINS as usize }>();
let segment_at = |chain_index: usize| -> Option<[u8; HASH_LEN]> {
let chain_value = signature.chains.get(chain_index).copied()?;
Some(wots_chain32_no_mask_base(
WOTS_CHAIN_LEN,
*pk_seed,
AddressBaseChain {
address_base,
chain_index: chain_index as u32,
},
chain_value,
digits[chain_index],
))
};
#[cfg(feature = "parallel")]
let filled = {
use rayon::prelude::*;
segments
.par_iter_mut()
.enumerate()
.all(|(chain_index, segment)| match segment_at(chain_index) {
Some(endpoint) => {
*segment = endpoint;
true
}
None => false,
})
};
#[cfg(not(feature = "parallel"))]
let filled = segments
.iter_mut()
.enumerate()
.all(|(chain_index, segment)| match segment_at(chain_index) {
Some(endpoint) => {
*segment = endpoint;
true
}
None => false,
});
if !filled {
return false;
}
let computed_pk_hash = stateless_wots_public_key_hash(pk_seed, segments.as_ref());
computed_pk_hash == *expected_pk_hash
}
#[derive(Clone, Copy)]
struct AddressBaseChain {
address_base: [u8; HASH_LEN],
chain_index: u32,
}
fn stateless_wots_chain_from_address_base(
pk_seed: &[u8; HASH_LEN],
addr: AddressBaseChain,
walk: ChainWalk,
) -> [u8; HASH_LEN] {
wots_chain_walk(
b"wots-c-chain",
pk_seed,
|step| wots_chain_address_word(addr.address_base, addr.chain_index, step),
walk,
)
}
fn wots_chain32_no_mask_base(
w: u16,
pk_seed: [u8; HASH_LEN],
addr: AddressBaseChain,
value: [u8; HASH_LEN],
digit: u32,
) -> [u8; HASH_LEN] {
let steps = u32::from(w - 1) - digit;
stateless_wots_chain_from_address_base(
&pk_seed,
addr,
ChainWalk {
value,
start: digit,
steps,
},
)
}
#[derive(Clone, Copy)]
struct HypertreePath {
layer: u32,
tree_index: u64,
leaf_index: u32,
}
fn hypertree_root_from_path32(
height: u32,
pk_seed: &[u8],
path: HypertreePath,
leaf: [u8; HASH_LEN],
auth_path: &[[u8; HASH_LEN]],
) -> Option<[u8; HASH_LEN]> {
if auth_path.len() != height as usize {
return None;
}
let pk_seed = word32(pk_seed)?;
crate::treehash::root_from_auth_path(
height,
path.leaf_index,
leaf,
auth_path,
|level, parent_index, left, right| {
let address_word =
hypertree_address_word(path.layer, path.tree_index, level, u64::from(parent_index));
hash_node(&[
b"hypertree-node".as_ref(),
pk_seed.as_ref(),
address_word.as_ref(),
left.as_ref(),
right.as_ref(),
])
},
)
}
use crate::trace_macros::stateless_trace_enabled;
#[derive(Clone, Copy)]
struct WotsKeypair {
layer: u32,
tree: u64,
keypair: u32,
}
impl WotsKeypair {
fn chain(self, chain: u32) -> WotsChain {
WotsChain {
layer: self.layer,
tree: self.tree,
keypair: self.keypair,
chain,
}
}
}
#[derive(Clone, Copy)]
struct WotsChain {
layer: u32,
tree: u64,
keypair: u32,
chain: u32,
}
struct WotsSeeds<'a> {
pk_seed: &'a [u8; HASH_LEN],
sk_seed: &'a [u8; HASH_LEN],
prf_seed: &'a [u8; HASH_LEN],
}
struct HypertreeSubtree {
root: [u8; HASH_LEN],
selected_leaf_hash: [u8; HASH_LEN],
auth_path: Vec<[u8; HASH_LEN]>,
}
pub(crate) fn sign_hypertree(
signing_key: &Key,
fors_root: [u8; HASH_LEN],
bottom_tree: u64,
bottom_leaf: u32,
) -> Option<Vec<LayerSignature>> {
if stateless_trace_enabled() {
hashsigs_println!(
"stateless trace: hypertree start bottom_tree={} bottom_leaf={} layers={}",
bottom_tree,
bottom_leaf,
NUM_HYPERTREE_LAYERS
);
}
let subtree_height = u32::from(HYPERTREE_HEIGHT / NUM_HYPERTREE_LAYERS);
if subtree_height == 0 || subtree_height >= u32::BITS {
return None;
}
let leaf_mask = (1u64 << subtree_height) - 1;
let layer_seeds = hypertree_layer_seeds(signing_key.secret().as_sk_seed().as_bytes());
let mut layers = Vec::with_capacity(NUM_HYPERTREE_LAYERS as usize);
let mut current = fors_root;
let mut tree = bottom_tree;
let mut leaf = bottom_leaf;
for layer in 0..u32::from(NUM_HYPERTREE_LAYERS) {
if stateless_trace_enabled() {
hashsigs_println!(
"stateless trace: hypertree layer={} tree={} leaf={}",
layer,
tree,
leaf
);
}
let subtree = hypertree_subtree(
signing_key.public_key.pk_seed.as_bytes(),
&layer_seeds[layer as usize],
layer,
tree,
leaf,
)?;
let coords = WotsKeypair {
layer,
tree,
keypair: leaf,
};
let (_, sk_seed) = hypertree_leaf_seeds(&layer_seeds[layer as usize], tree, leaf);
let seeds = WotsSeeds {
pk_seed: signing_key.public_key.pk_seed.as_bytes(),
sk_seed: &sk_seed,
prf_seed: signing_key.secret().as_prf_seed().as_bytes(),
};
let wots_c_signature =
sign_stateless_wots_c(&seeds, &coords, &subtree.selected_leaf_hash, ¤t)?;
let auth_path = subtree.auth_path;
current = subtree.root;
layers.push(LayerSignature {
wots_c_pk_hash: subtree.selected_leaf_hash,
wots_c_signature,
auth_path,
});
leaf = (tree & leaf_mask) as u32;
tree >>= subtree_height;
}
if stateless_trace_enabled() {
hashsigs_println!("stateless trace: hypertree complete");
}
Some(layers)
}
pub(crate) fn hypertree_public_root(
stateless_sk_seed: &[u8; HASH_LEN],
pk_seed: &[u8; HASH_LEN],
) -> [u8; HASH_LEN] {
let layer_seeds = hypertree_layer_seeds(stateless_sk_seed);
let top_layer = u32::from(NUM_HYPERTREE_LAYERS - 1);
match hypertree_subtree(pk_seed, &layer_seeds[top_layer as usize], top_layer, 0, 0) {
Some(subtree) => subtree.root,
None => {
[0u8; HASH_LEN]
}
}
}
fn hypertree_layer_seeds(
stateless_sk_seed: &[u8; HASH_LEN],
) -> [[u8; HASH_LEN]; NUM_HYPERTREE_LAYERS as usize] {
let mut seeds = [[0u8; HASH_LEN]; NUM_HYPERTREE_LAYERS as usize];
for (layer, seed) in seeds.iter_mut().enumerate() {
*seed = derive32(b"hypertree-layer-seed", stateless_sk_seed, &[layer as u8]);
}
seeds
}
fn hypertree_subtree(
pk_seed: &[u8; HASH_LEN],
layer_seed: &[u8; HASH_LEN],
layer: u32,
tree: u64,
selected_leaf: u32,
) -> Option<HypertreeSubtree> {
let subtree_height = u32::from(HYPERTREE_HEIGHT / NUM_HYPERTREE_LAYERS);
if subtree_height == 0 || subtree_height >= u32::BITS {
return None;
}
let leaf_count = 1u32 << subtree_height;
if selected_leaf >= leaf_count {
return None;
}
let selected_leaf_hash = hypertree_leaf(pk_seed, layer_seed, layer, tree, selected_leaf);
let (root, auth_path) = crate::treehash::treehash_root_and_auth_path(
subtree_height,
selected_leaf,
|leaf| {
if leaf == selected_leaf {
selected_leaf_hash
} else {
hypertree_leaf(pk_seed, layer_seed, layer, tree, leaf)
}
},
|node_height, parent_index, left, right| {
let address_word = hypertree_address_word(layer, tree, node_height, parent_index);
hash_node(&[
b"hypertree-node".as_ref(),
pk_seed.as_ref(),
address_word.as_ref(),
left.as_ref(),
right.as_ref(),
])
},
);
Some(HypertreeSubtree {
root,
selected_leaf_hash,
auth_path,
})
}
fn hypertree_leaf_seeds(
layer_seed: &[u8; HASH_LEN],
tree: u64,
leaf: u32,
) -> (Zeroizing<[u8; HASH_LEN]>, Zeroizing<[u8; HASH_LEN]>) {
let mut leaf_context = [0u8; 12];
leaf_context[..8].copy_from_slice(&tree.to_be_bytes());
leaf_context[8..].copy_from_slice(&leaf.to_be_bytes());
let leaf_seed = Zeroizing::new(derive32(b"hypertree-leaf-seed", layer_seed, &leaf_context));
let sk_seed = Zeroizing::new(derive32(b"hypertree-wots-sk-seed", &*leaf_seed, &[]));
(leaf_seed, sk_seed)
}
fn hypertree_leaf(
pk_seed: &[u8; HASH_LEN],
layer_seed: &[u8; HASH_LEN],
layer: u32,
tree: u64,
leaf: u32,
) -> [u8; HASH_LEN] {
let (_leaf_seed, sk_seed) = hypertree_leaf_seeds(layer_seed, tree, leaf);
let coords = WotsKeypair {
layer,
tree,
keypair: leaf,
};
stateless_wots_c_public_key(pk_seed, &sk_seed, &coords)
}
#[cfg(feature = "parallel")]
fn map_chains<T: Send>(n: usize, f: impl Fn(usize) -> T + Sync + Send) -> Vec<T> {
use rayon::prelude::*;
(0..n).into_par_iter().map(f).collect()
}
#[cfg(not(feature = "parallel"))]
fn map_chains<T>(n: usize, f: impl Fn(usize) -> T) -> Vec<T> {
(0..n).map(f).collect()
}
fn stateless_wots_c_public_key(
pk_seed: &[u8; HASH_LEN],
sk_seed: &[u8; HASH_LEN],
coords: &WotsKeypair,
) -> [u8; HASH_LEN] {
let chain_count = usize::from(NUM_WOTS_CHAINS);
let endpoint_at = |chain: usize| -> [u8; HASH_LEN] {
let secret = Zeroizing::new(stateless_wots_c_secret(sk_seed, chain as u32));
stateless_wots_c_chain(
pk_seed,
&coords.chain(chain as u32),
*secret,
0,
u32::from(WOTS_CHAIN_LEN - 1),
)
};
let endpoints: Vec<[u8; HASH_LEN]> = map_chains(chain_count, endpoint_at);
stateless_wots_public_key_hash(pk_seed, &endpoints)
}
fn sign_stateless_wots_c(
seeds: &WotsSeeds,
coords: &WotsKeypair,
pk_hash: &[u8; HASH_LEN],
message: &[u8; HASH_LEN],
) -> Option<Signature> {
let randomizer = hash_packed(&[b"wots-c-randomizer", seeds.prf_seed, message]);
let digest_bytes = wots_digest_bytes();
let result = crate::wots_c::grind_digit_sum(
WOTS_C_MAX_GRIND_COUNTER,
TARGET_SUM,
|counter| {
let digest = stateless_wots_message_digest(
seeds.pk_seed,
pk_hash,
&randomizer,
counter,
message,
);
let digest = &digest[..digest_bytes];
let mut digits = Vec::with_capacity(NUM_WOTS_CHAINS as usize);
let mut digit_sum = 0u32;
for index in 0..NUM_WOTS_CHAINS as usize {
let value = base_w_digit(WOTS_CHAIN_LEN, digest, index);
digit_sum = digit_sum.checked_add(value)?;
digits.push(value);
}
Some((digit_sum, digits))
},
|digits| {
let chain_at = |chain: usize, digit: u32| -> [u8; HASH_LEN] {
let secret = Zeroizing::new(stateless_wots_c_secret(seeds.sk_seed, chain as u32));
stateless_wots_c_chain(
seeds.pk_seed,
&coords.chain(chain as u32),
*secret,
0,
digit,
)
};
map_chains(digits.len(), |chain| chain_at(chain, digits[chain]))
},
)?;
let (counter, chains) = result;
Some(Signature {
randomizer,
counter,
chains,
})
}
fn stateless_wots_c_secret(sk_seed: &[u8; HASH_LEN], chain: u32) -> [u8; HASH_LEN] {
hash_packed(&[b"wots-c-secret", sk_seed, &chain.to_be_bytes()])
}
struct StatelessWotsChainCtx<'a> {
pk_seed: &'a [u8; HASH_LEN],
layer: u32,
tree: u64,
keypair: u32,
chain_index: u32,
}
fn stateless_wots_chain(ctx: &StatelessWotsChainCtx<'_>, walk: ChainWalk) -> [u8; HASH_LEN] {
use crate::hash::{address_word32, AddressWord32};
wots_chain_walk(
b"wots-c-chain",
ctx.pk_seed,
|step| {
address_word32(AddressWord32 {
layer: ctx.layer,
tree: ctx.tree,
address_type: 0,
keypair: ctx.keypair,
chain: ctx.chain_index,
step,
})
},
walk,
)
}
fn stateless_wots_c_chain(
pk_seed: &[u8; HASH_LEN],
coords: &WotsChain,
value: [u8; HASH_LEN],
start: u32,
steps: u32,
) -> [u8; HASH_LEN] {
let ctx = StatelessWotsChainCtx {
pk_seed,
layer: coords.layer,
tree: coords.tree,
keypair: coords.keypair,
chain_index: coords.chain,
};
stateless_wots_chain(
&ctx,
ChainWalk {
value,
start,
steps,
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wots_c::Signature as WotsCSignature;
use alloc::vec;
fn sample_layer_signature() -> LayerSignature {
LayerSignature {
wots_c_pk_hash: [0xA1; HASH_LEN],
wots_c_signature: WotsCSignature {
randomizer: [0xA2; HASH_LEN],
counter: 0x0C0F_FEEDu32,
chains: vec![[0xA3; HASH_LEN], [0xA4; HASH_LEN], [0xA5; HASH_LEN]],
},
auth_path: vec![[0xA6; HASH_LEN], [0xA7; HASH_LEN]],
}
}
#[test]
fn layer_signature_to_bytes_from_bytes_round_trips() {
let layer = sample_layer_signature();
let encoded = layer.to_bytes();
let decoded = LayerSignature::from_bytes(&encoded).expect("valid encoding must decode");
assert_eq!(decoded, layer);
assert_eq!(decoded.to_bytes(), encoded);
}
#[test]
fn layer_signature_from_bytes_rejects_trailing_bytes() {
let mut encoded = sample_layer_signature().to_bytes();
encoded.push(0x00);
assert!(
LayerSignature::from_bytes(&encoded).is_none(),
"trailing junk on a standalone layer body must be rejected"
);
encoded.pop();
encoded.extend_from_slice(&[0xAA, 0xBB]);
assert!(LayerSignature::from_bytes(&encoded).is_none());
}
#[test]
fn layer_signature_from_bytes_rejects_truncated() {
let encoded = sample_layer_signature().to_bytes();
assert!(LayerSignature::from_bytes(&encoded[..encoded.len() - 1]).is_none());
assert!(LayerSignature::from_bytes(&[]).is_none());
}
#[test]
fn layer_signature_try_from_delegates_to_from_bytes() {
let layer = sample_layer_signature();
let encoded = layer.to_bytes();
let decoded =
LayerSignature::try_from(encoded.as_slice()).expect("valid encoding must decode");
assert_eq!(decoded, layer);
assert!(LayerSignature::try_from(&encoded[..encoded.len() - 1]).is_err());
}
#[cfg(not(any(feature = "profile-128s-q18", feature = "profile-128s-q20")))]
#[test]
fn hypertree_sign_verify_round_trip() {
let key =
crate::sphincs_plus_c::keygen([0x11; HASH_LEN], [0x22; HASH_LEN], [0x33; HASH_LEN]);
let fors_root = [0xABu8; HASH_LEN];
let seed = HypertreeSeed {
tree_index: 0,
leaf_index: 3,
};
let layers = sign_hypertree(&key, fors_root, seed.tree_index, seed.leaf_index)
.expect("hypertree sign must succeed");
assert_eq!(layers.len(), NUM_HYPERTREE_LAYERS as usize);
for layer in &layers {
assert_eq!(layer.auth_path.len(), HYPERTREE_SUBTREE_HEIGHT);
let encoded = layer.to_bytes();
let decoded = LayerSignature::from_bytes(&encoded).expect("layer codec");
assert_eq!(decoded, *layer);
}
assert!(
verify_hypertree(
key.public_key.pk_seed.as_bytes(),
key.public_key.root.as_bytes(),
fors_root,
seed,
&layers,
),
"fresh hypertree signature must verify against keygen root"
);
assert!(!verify_hypertree(
key.public_key.pk_seed.as_bytes(),
key.public_key.root.as_bytes(),
[0xCDu8; HASH_LEN],
seed,
&layers,
));
assert!(!verify_hypertree(
key.public_key.pk_seed.as_bytes(),
key.public_key.root.as_bytes(),
fors_root,
HypertreeSeed {
tree_index: seed.tree_index,
leaf_index: seed.leaf_index ^ 1,
},
&layers,
));
}
#[test]
fn verify_hypertree_rejects_wrong_layer_count() {
let pk_seed = [0x01u8; HASH_LEN];
let root = [0x02u8; HASH_LEN];
let fors_root = [0x03u8; HASH_LEN];
let seed = HypertreeSeed {
tree_index: 0,
leaf_index: 0,
};
assert!(!verify_hypertree(&pk_seed, &root, fors_root, seed, &[]));
let extra = vec![sample_layer_signature(); NUM_HYPERTREE_LAYERS as usize + 1];
assert!(!verify_hypertree(&pk_seed, &root, fors_root, seed, &extra));
}
}