Skip to content

Commit 74486ad

Browse files
Validate public IP doesn't clash with dz_prefix
Resolves: #1836
1 parent 6740b90 commit 74486ad

File tree

10 files changed

+199
-30
lines changed

10 files changed

+199
-30
lines changed

e2e/device_telemetry_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,10 @@ func TestE2E_DeviceTelemetry(t *testing.T) {
171171
log.Info("==> Adding dummy devices onchain")
172172
_, err = dn.Manager.Exec(t.Context(), []string{"bash", "-c", `
173173
set -euo pipefail
174-
doublezero device create --code ld4-dz01 --contributor co01 --location lhr --exchange xlhr --public-ip "195.219.120.72" --dz-prefixes "195.219.120.72/29" --mgmt-vrf mgmt
175-
doublezero device create --code frk-dz01 --contributor co01 --location fra --exchange xfra --public-ip "195.219.220.88" --dz-prefixes "195.219.220.88/29" --mgmt-vrf mgmt
176-
doublezero device create --code sg1-dz01 --contributor co01 --location sin --exchange xsin --public-ip "180.87.102.104" --dz-prefixes "180.87.102.104/29" --mgmt-vrf mgmt
177-
doublezero device create --code ty2-dz01 --contributor co01 --location tyo --exchange xtyo --public-ip "180.87.154.112" --dz-prefixes "180.87.154.112/29" --mgmt-vrf mgmt
174+
doublezero device create --code ld4-dz01 --contributor co01 --location lhr --exchange xlhr --public-ip "195.219.120.72" --dz-prefixes "195.219.120.80/29" --mgmt-vrf mgmt
175+
doublezero device create --code frk-dz01 --contributor co01 --location fra --exchange xfra --public-ip "195.219.220.88" --dz-prefixes "195.219.220.96/29" --mgmt-vrf mgmt
176+
doublezero device create --code sg1-dz01 --contributor co01 --location sin --exchange xsin --public-ip "180.87.102.104" --dz-prefixes "180.87.102.112/29" --mgmt-vrf mgmt
177+
doublezero device create --code ty2-dz01 --contributor co01 --location tyo --exchange xtyo --public-ip "180.87.154.112" --dz-prefixes "180.87.154.120/29" --mgmt-vrf mgmt
178178
doublezero device create --code pit-dzd01 --contributor co01 --location pit --exchange xpit --public-ip "204.16.241.243" --dz-prefixes "204.16.243.243/32" --mgmt-vrf mgmt
179179
doublezero device create --code ams-dz001 --contributor co01 --location ams --exchange xams --public-ip "195.219.138.50" --dz-prefixes "195.219.138.56/29" --mgmt-vrf mgmt
180180
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
account | code | contributor | location | exchange | device_type | public_ip | dz_prefixes | users | max_users | status | owner | mgmt_vrf
22
IGNORED | la2-dz01 | co01 | lax | xlax | switch | 207.45.216.134 | 207.45.216.136/30, 200.12.12.12/29 | 5 | 128 | activated | {{.ManagerPubkey}} | mgmt
3-
IGNORED | ty2-dz01 | co01 | tyo | xtyo | switch | 180.87.154.112 | 180.87.154.112/29 | 0 | 128 | activated | {{.ManagerPubkey}} | mgmt
3+
IGNORED | ty2-dz01 | co01 | tyo | xtyo | switch | 180.87.154.112 | 180.87.154.120/29 | 0 | 128 | activated | {{.ManagerPubkey}} | mgmt
44
IGNORED | ams-dz001 | co01 | ams | xams | switch | 195.219.138.50 | 195.219.138.56/29 | 0 | 128 | activated | {{.ManagerPubkey}} | mgmt
55
IGNORED | pit-dzd01 | co01 | pit | xpit | switch | 204.16.241.243 | 204.16.243.243/32 | 0 | 128 | activated | {{.ManagerPubkey}} | mgmt
6-
IGNORED | frk-dz01 | co01 | fra | xfra | switch | 195.219.220.88 | 195.219.220.88/29 | 0 | 128 | activated | {{.ManagerPubkey}} | mgmt
7-
IGNORED | sg1-dz01 | co01 | sin | xsin | switch | 180.87.102.104 | 180.87.102.104/29 | 0 | 128 | activated | {{.ManagerPubkey}} | mgmt
8-
IGNORED | ny5-dz01 | co01 | ewr | xewr | switch | {{.DeviceIP}} | {{.DeviceIP}}/{{.DeviceAllocatablePrefix}}| 1 | 128 | activated | {{.ManagerPubkey}} | mgmt
9-
IGNORED | ld4-dz01 | co01 | lhr | xlhr | switch | 195.219.120.72 | 195.219.120.72/29 | 0 | 128 | activated | {{.ManagerPubkey}} | mgmt
6+
IGNORED | frk-dz01 | co01 | fra | xfra | switch | 195.219.220.88 | 195.219.220.96/29 | 0 | 128 | activated | {{.ManagerPubkey}} | mgmt
7+
IGNORED | sg1-dz01 | co01 | sin | xsin | switch | 180.87.102.104 | 180.87.102.112/29 | 0 | 128 | activated | {{.ManagerPubkey}} | mgmt
8+
IGNORED | ny5-dz01 | co01 | ewr | xewr | switch | {{.DeviceIP}} | {{.DeviceDZPrefix}} | 1 | 128 | activated | {{.ManagerPubkey}} | mgmt
9+
IGNORED | ld4-dz01 | co01 | lhr | xlhr | switch | 195.219.120.72 | 195.219.120.80/29 | 0 | 128 | activated | {{.ManagerPubkey}} | mgmt

e2e/ibrl_with_allocated_ip_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ package e2e_test
44

55
import (
66
"fmt"
7-
"strconv"
87
"strings"
98
"testing"
109
"time"
@@ -133,9 +132,9 @@ func checkIBRLWithAllocatedIPPostConnect(t *testing.T, dn *TestDevnet, device *d
133132
name: "doublezero_device_list",
134133
fixturePath: "fixtures/ibrl_with_allocated_addr/doublezero_device_list.tmpl",
135134
data: map[string]any{
136-
"DeviceIP": device.CYOANetworkIP,
137-
"ManagerPubkey": dn.Manager.Pubkey,
138-
"DeviceAllocatablePrefix": strconv.Itoa(int(device.Spec.CYOANetworkAllocatablePrefix)),
135+
"DeviceIP": device.CYOANetworkIP,
136+
"ManagerPubkey": dn.Manager.Pubkey,
137+
"DeviceDZPrefix": device.DZPrefix,
139138
},
140139
cmd: []string{"doublezero", "device", "list"},
141140
},

e2e/internal/devnet/device.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ type Device struct {
172172

173173
ContainerID string
174174
CYOANetworkIP string
175+
DZPrefix string // The dz_prefix registered onchain for this device
175176

176177
// ExternalEAPIHTTPPort is the port on which the device's EAPI HTTP server is exposed.
177178
ExternalEAPIHTTPPort int
@@ -283,11 +284,32 @@ func (d *Device) Start(ctx context.Context) error {
283284
}
284285

285286
// Create the device onchain.
286-
devicePK, err := d.dn.GetOrCreateDeviceOnchain(ctx, spec.Code, spec.Location, spec.Exchange, spec.MetricsPublisherPK, cyoaNetworkIP, []string{cyoaNetworkIP + "/" + strconv.Itoa(int(spec.CYOANetworkAllocatablePrefix))}, "mgmt")
287+
// Use a different IP range for dz_prefixes to avoid conflicts with the public IP.
288+
// We derive a unique /29 subnet by taking the public IP and adding 128 to the last octet,
289+
// then rounding down to a /29 boundary (multiples of 8).
290+
// For example, if public IP is 10.237.248.8, we use 10.237.248.136/29 as dz_prefix.
291+
// This ensures each device gets a unique dz_prefix that doesn't overlap with public IPs.
292+
publicIP := net.ParseIP(cyoaNetworkIP)
293+
if publicIP == nil {
294+
return fmt.Errorf("failed to parse public IP: %s", cyoaNetworkIP)
295+
}
296+
publicIPBytes := publicIP.To4()
297+
if publicIPBytes == nil {
298+
return fmt.Errorf("public IP is not IPv4: %s", cyoaNetworkIP)
299+
}
300+
// Add 128 to the last octet to create separation from public IPs,
301+
// then round down to /29 boundary (multiple of 8)
302+
dzPrefixBytes := make(net.IP, 4)
303+
copy(dzPrefixBytes, publicIPBytes)
304+
dzPrefixBytes[3] = ((publicIPBytes[3] + 128) / 8) * 8
305+
dzPrefix := dzPrefixBytes.String() + "/29"
306+
d.DZPrefix = dzPrefix
307+
308+
devicePK, err := d.dn.GetOrCreateDeviceOnchain(ctx, spec.Code, spec.Location, spec.Exchange, spec.MetricsPublisherPK, cyoaNetworkIP, []string{dzPrefix}, "mgmt")
287309
if err != nil {
288310
return fmt.Errorf("failed to create device %s onchain: %w", spec.Code, err)
289311
}
290-
d.log.Info("--> Created device onchain", "code", spec.Code, "cyoaNetworkIP", cyoaNetworkIP, "devicePK", devicePK)
312+
d.log.Info("--> Created device onchain", "code", spec.Code, "cyoaNetworkIP", cyoaNetworkIP, "dzPrefix", dzPrefix, "devicePK", devicePK)
291313

292314
// MaxUserTunnelSlots is now a constant from config package
293315
d.log.Info("--> Using MaxUserTunnelSlots constant", "maxUsers", controllerconfig.MaxUserTunnelSlots)

e2e/main_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,10 @@ func (dn *TestDevnet) Start(t *testing.T) (*devnet.Device, *devnet.Client) {
166166
set -euo pipefail
167167
168168
echo "==> Populate device information onchain"
169-
doublezero device create --code ld4-dz01 --contributor co01 --location lhr --exchange xlhr --public-ip "195.219.120.72" --dz-prefixes "195.219.120.72/29" --mgmt-vrf mgmt
170-
doublezero device create --code frk-dz01 --contributor co01 --location fra --exchange xfra --public-ip "195.219.220.88" --dz-prefixes "195.219.220.88/29" --mgmt-vrf mgmt
171-
doublezero device create --code sg1-dz01 --contributor co01 --location sin --exchange xsin --public-ip "180.87.102.104" --dz-prefixes "180.87.102.104/29" --mgmt-vrf mgmt
172-
doublezero device create --code ty2-dz01 --contributor co01 --location tyo --exchange xtyo --public-ip "180.87.154.112" --dz-prefixes "180.87.154.112/29" --mgmt-vrf mgmt
169+
doublezero device create --code ld4-dz01 --contributor co01 --location lhr --exchange xlhr --public-ip "195.219.120.72" --dz-prefixes "195.219.120.80/29" --mgmt-vrf mgmt
170+
doublezero device create --code frk-dz01 --contributor co01 --location fra --exchange xfra --public-ip "195.219.220.88" --dz-prefixes "195.219.220.96/29" --mgmt-vrf mgmt
171+
doublezero device create --code sg1-dz01 --contributor co01 --location sin --exchange xsin --public-ip "180.87.102.104" --dz-prefixes "180.87.102.112/29" --mgmt-vrf mgmt
172+
doublezero device create --code ty2-dz01 --contributor co01 --location tyo --exchange xtyo --public-ip "180.87.154.112" --dz-prefixes "180.87.154.120/29" --mgmt-vrf mgmt
173173
doublezero device create --code pit-dzd01 --contributor co01 --location pit --exchange xpit --public-ip "204.16.241.243" --dz-prefixes "204.16.243.243/32" --mgmt-vrf mgmt
174174
doublezero device create --code ams-dz001 --contributor co01 --location ams --exchange xams --public-ip "195.219.138.50" --dz-prefixes "195.219.138.56/29" --mgmt-vrf mgmt
175175
echo "--> Device information onchain:"

e2e/sdk_device_telemetry_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ func TestE2E_SDK_Telemetry_DeviceLatencySamples(t *testing.T) {
5959
6060
doublezero device create --code la2-dz01 --contributor co01 --location lax --exchange xlax --public-ip "207.45.216.134" --dz-prefixes "207.45.216.136/30,200.12.12.12/29" --metrics-publisher ` + la2DeviceAgentPrivateKey.PublicKey().String() + ` --mgmt-vrf mgmt
6161
doublezero device create --code ny5-dz01 --contributor co01 --location ewr --exchange xewr --public-ip "207.45.21.134" --dz-prefixes "200.12.12.12/29" --metrics-publisher ` + ny5DeviceAgentPrivateKey.PublicKey().String() + ` --mgmt-vrf mgmt
62-
doublezero device create --code ld4-dz01 --contributor co01 --location lhr --exchange xlhr --public-ip "195.219.120.72" --dz-prefixes "195.219.120.72/29" --mgmt-vrf mgmt
63-
doublezero device create --code frk-dz01 --contributor co01 --location fra --exchange xfra --public-ip "195.219.220.88" --dz-prefixes "195.219.220.88/29" --mgmt-vrf mgmt
62+
doublezero device create --code ld4-dz01 --contributor co01 --location lhr --exchange xlhr --public-ip "195.219.120.72" --dz-prefixes "195.219.120.80/29" --mgmt-vrf mgmt
63+
doublezero device create --code frk-dz01 --contributor co01 --location fra --exchange xfra --public-ip "195.219.220.88" --dz-prefixes "195.219.220.96/29" --mgmt-vrf mgmt
6464
6565
doublezero device update --pubkey ld4-dz01 --max-users 128
6666
doublezero device update --pubkey frk-dz01 --max-users 128

smartcontract/cli/src/device/create.rs

Lines changed: 132 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,33 @@ impl CreateDeviceCliCommand {
5757

5858
let devices = client.list_device(ListDeviceCommand)?;
5959
if devices.iter().any(|(_, d)| d.code == self.code) {
60-
return Err(eyre::eyre!(
61-
"Device with code '{}' already exists",
62-
self.code
63-
));
60+
eyre::bail!("Device with code '{}' already exists", self.code);
6461
}
6562
if devices.iter().any(|(_, d)| d.public_ip == self.public_ip) {
66-
return Err(eyre::eyre!(
67-
"Device with public ip '{}' already exists",
68-
&self.public_ip
69-
));
63+
eyre::bail!("Device with public ip '{}' already exists", &self.public_ip);
64+
}
65+
66+
for device in devices.values() {
67+
for dz_prefix in device.dz_prefixes.iter() {
68+
if dz_prefix.contains(self.public_ip) {
69+
eyre::bail!(
70+
"Public IP '{}' conflicts with existing device '{}' dz_prefix '{}'",
71+
self.public_ip,
72+
device.code,
73+
dz_prefix
74+
);
75+
}
76+
}
77+
}
78+
79+
for dz_prefix in self.dz_prefixes.iter() {
80+
if dz_prefix.contains(self.public_ip) {
81+
eyre::bail!(
82+
"Public IP '{}' conflicts with device's own dz_prefix '{}'",
83+
self.public_ip,
84+
dz_prefix
85+
);
86+
}
7087
}
7188

7289
let contributor_pk = match parse_pubkey(&self.contributor) {
@@ -111,7 +128,7 @@ impl CreateDeviceCliCommand {
111128
} else {
112129
match Pubkey::from_str(metrics_publisher) {
113130
Ok(pk) => pk,
114-
Err(_) => return Err(eyre::eyre!("Invalid metrics publisher Pubkey")),
131+
Err(_) => eyre::bail!("Invalid metrics publisher Pubkey"),
115132
}
116133
}
117134
} else {
@@ -279,4 +296,110 @@ mod tests {
279296
output_str,"Signature: 3QnHBSdd4doEF6FgpLCejqEw42UQjfvNhQJwoYDSpoBszpCCqVft4cGoneDCnZ6Ez3ujzavzUu85u6F79WtLhcsv\n"
280297
);
281298
}
299+
300+
#[test]
301+
fn test_cli_device_create_fails_when_public_ip_conflicts_with_existing_device_dz_prefix() {
302+
use doublezero_sdk::{Device, DeviceStatus};
303+
304+
let mut client = create_test_client();
305+
306+
let location_pk = Pubkey::from_str_const("HQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx");
307+
let exchange_pk = Pubkey::from_str_const("HQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcc");
308+
let contributor_pk = Pubkey::from_str_const("HQ3UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx");
309+
310+
// Create an existing device with dz_prefix that will conflict
311+
let existing_device_pk =
312+
Pubkey::from_str_const("HQ4UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx");
313+
let existing_device = Device {
314+
account_type: AccountType::Device,
315+
index: 1,
316+
bump_seed: 255,
317+
reference_count: 0,
318+
code: "existing-device".to_string(),
319+
contributor_pk,
320+
location_pk,
321+
exchange_pk,
322+
device_type: DeviceType::Switch,
323+
public_ip: [100, 0, 0, 1].into(),
324+
// This dz_prefix includes 10.1.5.10
325+
dz_prefixes: "10.1.0.0/16".parse().unwrap(),
326+
metrics_publisher_pk: Pubkey::default(),
327+
status: DeviceStatus::Activated,
328+
mgmt_vrf: String::default(),
329+
interfaces: vec![],
330+
users_count: 0,
331+
max_users: 100,
332+
owner: Pubkey::default(),
333+
};
334+
335+
let mut devices = HashMap::new();
336+
devices.insert(existing_device_pk, existing_device);
337+
338+
client
339+
.expect_check_requirements()
340+
.with(predicate::eq(CHECK_ID_JSON | CHECK_BALANCE))
341+
.returning(|_| Ok(()));
342+
client
343+
.expect_list_device()
344+
.with(predicate::eq(ListDeviceCommand))
345+
.returning(move |_| Ok(devices.clone()));
346+
347+
let mut output = Vec::new();
348+
// Create a device with public_ip 10.1.5.10, which is within existing device's dz_prefix
349+
let res = CreateDeviceCliCommand {
350+
code: "new-device".to_string(),
351+
contributor: contributor_pk.to_string(),
352+
location: location_pk.to_string(),
353+
exchange: exchange_pk.to_string(),
354+
public_ip: [10, 1, 5, 10].into(), // This is within 10.1.0.0/16
355+
dz_prefixes: "192.168.0.0/16".parse().unwrap(),
356+
metrics_publisher: Some(Pubkey::default().to_string()),
357+
mgmt_vrf: String::default(),
358+
wait: false,
359+
}
360+
.execute(&client, &mut output);
361+
362+
assert!(res.is_err());
363+
let err = res.unwrap_err();
364+
assert!(err.to_string().contains("Public IP '10.1.5.10' conflicts with existing device 'existing-device' dz_prefix '10.1.0.0/16'"));
365+
}
366+
367+
#[test]
368+
fn test_cli_device_create_fails_when_public_ip_conflicts_with_own_dz_prefix() {
369+
let mut client = create_test_client();
370+
371+
let location_pk = Pubkey::from_str_const("HQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx");
372+
let exchange_pk = Pubkey::from_str_const("HQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcc");
373+
let contributor_pk = Pubkey::from_str_const("HQ3UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx");
374+
375+
client
376+
.expect_check_requirements()
377+
.with(predicate::eq(CHECK_ID_JSON | CHECK_BALANCE))
378+
.returning(|_| Ok(()));
379+
client
380+
.expect_list_device()
381+
.with(predicate::eq(ListDeviceCommand))
382+
.returning(move |_| Ok(HashMap::new()));
383+
384+
let mut output = Vec::new();
385+
// Create a device where public_ip is within its own dz_prefix
386+
let res = CreateDeviceCliCommand {
387+
code: "test-device".to_string(),
388+
contributor: contributor_pk.to_string(),
389+
location: location_pk.to_string(),
390+
exchange: exchange_pk.to_string(),
391+
public_ip: [10, 1, 5, 10].into(), // This is within 10.1.0.0/16
392+
dz_prefixes: "10.1.0.0/16".parse().unwrap(), // Own prefix contains public_ip
393+
metrics_publisher: Some(Pubkey::default().to_string()),
394+
mgmt_vrf: String::default(),
395+
wait: false,
396+
}
397+
.execute(&client, &mut output);
398+
399+
assert!(res.is_err());
400+
let err = res.unwrap_err();
401+
assert!(err
402+
.to_string()
403+
.contains("Public IP '10.1.5.10' conflicts with device's own dz_prefix '10.1.0.0/16'"));
404+
}
282405
}

smartcontract/programs/common/src/types/network_v4.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ impl NetworkV4 {
2626
pub fn nth(&self, n: u32) -> Option<Ipv4Addr> {
2727
self.0.nth(n)
2828
}
29+
30+
pub fn contains(&self, ip: Ipv4Addr) -> bool {
31+
self.0.contains(ip)
32+
}
2933
}
3034

3135
impl Default for NetworkV4 {

smartcontract/programs/doublezero-serviceability/src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ pub enum DoubleZeroError {
117117
InvalidBgpCommunity, // variant 55
118118
#[error("Interface already exists")]
119119
InterfaceAlreadyExists, // variant 56
120+
#[error("Invalid Public IP: IP conflicts with DZ prefix")]
121+
InvalidPublicIp, // variant 57
120122
}
121123

122124
impl From<DoubleZeroError> for ProgramError {
@@ -179,6 +181,7 @@ impl From<DoubleZeroError> for ProgramError {
179181
DoubleZeroError::UserAccountNotFound => ProgramError::Custom(54),
180182
DoubleZeroError::InvalidBgpCommunity => ProgramError::Custom(55),
181183
DoubleZeroError::InterfaceAlreadyExists => ProgramError::Custom(56),
184+
DoubleZeroError::InvalidPublicIp => ProgramError::Custom(57),
182185
}
183186
}
184187
}
@@ -242,6 +245,7 @@ impl From<u32> for DoubleZeroError {
242245
54 => DoubleZeroError::UserAccountNotFound,
243246
55 => DoubleZeroError::InvalidBgpCommunity,
244247
56 => DoubleZeroError::InterfaceAlreadyExists,
248+
57 => DoubleZeroError::InvalidPublicIp,
245249
_ => DoubleZeroError::Custom(e),
246250
}
247251
}
@@ -320,6 +324,11 @@ mod tests {
320324
InvalidVlanId,
321325
InvalidMaxBandwidth,
322326
InvalidMulticastIp,
327+
InvalidAccountOwner,
328+
AccessPassNotFound,
329+
UserAccountNotFound,
330+
InvalidBgpCommunity,
331+
InvalidPublicIp,
323332
];
324333
for err in variants {
325334
let pe: ProgramError = err.clone().into();

smartcontract/programs/doublezero-serviceability/src/processors/device/create.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ pub fn process_create_device(
117117
location.reference_count += 1;
118118
exchange.reference_count += 1;
119119

120+
for prefix in value.dz_prefixes.iter() {
121+
if prefix.contains(value.public_ip) {
122+
#[cfg(test)]
123+
msg!(
124+
"Public IP {} conflicts with dz_prefix {}",
125+
value.public_ip,
126+
prefix
127+
);
128+
return Err(DoubleZeroError::InvalidPublicIp.into());
129+
}
130+
}
131+
120132
let device: Device = Device {
121133
account_type: AccountType::Device,
122134
owner: *payer_account.key,

0 commit comments

Comments
 (0)