feat(core): run bounded real gateway sessions

This commit is contained in:
sechmachine
2026-08-12 22:03:10 +07:00
parent 86a95952b6
commit 519c04e18f
10 changed files with 1598 additions and 178 deletions
+326 -27
View File
@@ -1,5 +1,8 @@
use std::ffi::c_void;
use std::fs;
use std::mem::size_of;
use std::process::{Child, Command, Stdio};
use std::ptr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
@@ -13,7 +16,8 @@ use rustls::SignatureScheme;
use serde::Deserialize;
use versevdi_core::error::CoreError;
use versevdi_core::transport::{
connect, connect_with_cancellation, AdmissionSigner, Cancellation, Signers, TlsEd25519Signer,
bounded_session_events, connect, connect_with_cancellation, AdmissionSigner, Cancellation,
SessionCommand, SessionEvent, Signers, TlsEd25519Signer,
};
use versevdi_core::wire::{ConnectionManifest, NativeTunnelCredential};
@@ -76,8 +80,8 @@ func (a *admission) Release(context.Context, protocol.SessionAuthority) error {
return nil
}
type provider struct { admission *admission; failStart bool; delay time.Duration }
type session struct { state protocol.ProviderState; video chan gateway.ProviderMedia; audio chan gateway.ProviderMedia; events chan gateway.ProviderEvent }
type provider struct { admission *admission; failStart bool; delay time.Duration; mode string; inputPath string; releasePath string }
type session struct { state protocol.ProviderState; video chan gateway.ProviderMedia; audio chan gateway.ProviderMedia; events chan gateway.ProviderEvent; mode string; inputPath string; releasePath string }
func (p provider) Start(_ context.Context, request gateway.LaunchRequest) (gateway.ProviderSession, error) {
p.admission.mu.Lock()
p.admission.providerStarts++
@@ -86,18 +90,24 @@ func (p provider) Start(_ context.Context, request gateway.LaunchRequest) (gatew
p.admission.mu.Unlock()
if p.failStart { return nil, context.DeadlineExceeded }
if p.delay > 0 { time.Sleep(p.delay) }
return &session{state: protocol.ProviderState{Version:"1", SessionID:request.SessionID, State:gateway.ProviderStateReady, Channels:[]string{"video","audio","input","feedback"}}, video:make(chan gateway.ProviderMedia), audio:make(chan gateway.ProviderMedia), events:make(chan gateway.ProviderEvent)}, nil
current := &session{state: protocol.ProviderState{Version:"1", SessionID:request.SessionID, State:gateway.ProviderStateReady, Channels:[]string{"video","audio","input","feedback"}}, video:make(chan gateway.ProviderMedia,2), audio:make(chan gateway.ProviderMedia,2), events:make(chan gateway.ProviderEvent,2), mode:p.mode, inputPath:p.inputPath, releasePath:p.releasePath}
if p.mode == "io" || p.mode == "input" {
current.video <- gateway.ProviderMedia{Payload:bytes.Repeat([]byte{0xA5}, 2000)}
current.audio <- gateway.ProviderMedia{Payload:[]byte{0x10,0x20,0x30}}
current.events <- gateway.ProviderEvent{Kind:gateway.ProviderEventRumble,Payload:[]byte{0,0,1,0,2}}
}
return current, nil
}
func (s *session) Ready(context.Context) error { return nil }
func (s *session) Video() <-chan gateway.ProviderMedia { return s.video }
func (s *session) Audio() <-chan gateway.ProviderMedia { return s.audio }
func (s *session) Events() <-chan gateway.ProviderEvent { return s.events }
func (s *session) Input(context.Context, gateway.InputEvent) error { return nil }
func (s *session) Input(_ context.Context, event gateway.InputEvent) error { if s.mode == "slow-input" { time.Sleep(100*time.Millisecond) }; data,_ := json.Marshal(event); if err := os.WriteFile(s.inputPath,data,0600); err != nil { panic(err) }; return nil }
func (s *session) Feedback(context.Context, gateway.Feedback) error { return nil }
func (s *session) ReadClipboard(context.Context) (string, error) { return "", errors.New("disabled") }
func (s *session) WriteClipboard(context.Context, string) error { return errors.New("disabled") }
func (s *session) Telemetry() gateway.ProviderTelemetry { return gateway.ProviderTelemetry{State:s.state.State} }
func (s *session) ReleaseAll(context.Context) error { return nil }
func (s *session) ReleaseAll(context.Context) error { return os.WriteFile(s.releasePath,[]byte("released"),0600) }
func (s *session) Terminate(context.Context) error { return nil }
func (s *session) State() protocol.ProviderState { return s.state }
@@ -128,11 +138,11 @@ func main() {
if mode == "session" { authority.SessionID = "other-session" }
if mode == "gateway" { authority.GatewayID = "other-gateway" }
if mode == "audience" { authority.Audience = "other-audience" }
if mode == "reconnect" { authority.ReconnectSequence = 1 }
if mode == "reconnect" || mode == "reconnect-success" { authority.ReconnectSequence = 1 }
if mode == "expiry" { authority.ExpiresAt = time.Now().Add(-time.Minute).UTC().Truncate(time.Second).Format(time.RFC3339) }
if mode == "capability" { authority.Capabilities.ClientDecode = []string{"hevc-opus"} }
if mode == "alpn" { server.NextProtos = []string{"wrong-alpn"} }
work := protocol.ProviderSessionWork{Version:"1",SessionID:authority.SessionID,GatewayID:authority.GatewayID,ExpiresAt:expiry,ProviderProfile:gateway.ProviderProfileApollo,ProviderIdentity:authority.ProviderIdentity,PolicyVersionID:"policy-1",StreamPolicy:protocol.ProviderStreamPolicy{ResolutionWidth:1920,ResolutionHeight:1080,Fps:60,Codec:"H264",BitrateKbps:8000,AudioEnabled:true},ApplicationID:"1",ClientID:"client",ManagementHost:"provider.invalid",ManagementPort:47990,StreamHost:"provider.invalid",StreamPort:47984,ClientCertificatePem:"certificate",ClientPrivateKeyPem:"private-key",ServerCertificatePem:"certificate",ClipboardPolicy:protocol.ClipboardPolicy{MaxTextBytes:65536,MaxUpdatesPerMinute:30}}
work := protocol.ProviderSessionWork{Version:"1",SessionID:authority.SessionID,GatewayID:authority.GatewayID,ReconnectSequence:authority.ReconnectSequence,ExpiresAt:expiry,ProviderProfile:gateway.ProviderProfileApollo,ProviderIdentity:authority.ProviderIdentity,PolicyVersionID:"policy-1",StreamPolicy:protocol.ProviderStreamPolicy{ResolutionWidth:1920,ResolutionHeight:1080,Fps:60,Codec:"H264",BitrateKbps:8000,AudioEnabled:true},ApplicationID:"1",ClientID:"client",ManagementHost:"provider.invalid",ManagementPort:47990,StreamHost:"provider.invalid",StreamPort:47984,ClientCertificatePem:"certificate",ClientPrivateKeyPem:"private-key",ServerCertificatePem:"certificate",ClipboardPolicy:protocol.ClipboardPolicy{MaxTextBytes:65536,MaxUpdatesPerMinute:30}}
replayGuard := &admission{authority:authority,work:work,public:admissionKey.Public().(ed25519.PublicKey)}
replayRequest := protocol.TunnelAdmissionRequest{Version:"1",SessionID:"session-1",GatewayID:"gateway-1",Audience:"versevdi-gateway",Grant:"ggggggggggggggggggggggggggggggggggggggggggg",ClientNonce:"bm9uY2UtZm9yLXJlcGxheQ",Capabilities:gateway.DefaultCapabilities()}
replayRequest.DeviceSignature = base64.RawURLEncoding.EncodeToString(ed25519.Sign(admissionKey,replayRequest.DeviceAdmissionTranscript()))
@@ -140,10 +150,12 @@ func main() {
_, secondReplayErr := replayGuard.Admit(context.Background(),replayRequest)
replayRejected := firstReplayErr == nil && errors.Is(secondReplayErr,gateway.ErrAdmissionRejected)
mutationPath := readyPath + ".provider-starts"
inputPath := readyPath + ".input"
releasePath := readyPath + ".release-all"
if err := os.WriteFile(mutationPath, []byte("0"), 0600); err != nil { panic(err) }
admissionService := &admission{authority:authority,work:work,public:admissionKey.Public().(ed25519.PublicKey),reusable:mode == "reusable",failWorkOnce:mode == "post-retryable",releaseFails:mode == "cleanup-release-failure",mutationPath:mutationPath}
newService := func(delay time.Duration) *gateway.Server {
service, err := gateway.NewServer(gateway.ServerConfig{ListenAddress:"127.0.0.1:0",TLSConfig:server,GatewayID:authority.GatewayID,Admission:admissionService,Provider:provider{admission:admissionService,failStart:mode == "provider-start-lost-response" || mode == "cleanup-release-failure",delay:delay}}); if err != nil { panic(err) }
service, err := gateway.NewServer(gateway.ServerConfig{ListenAddress:"127.0.0.1:0",TLSConfig:server,GatewayID:authority.GatewayID,Admission:admissionService,Provider:provider{admission:admissionService,failStart:mode == "provider-start-lost-response" || mode == "cleanup-release-failure",delay:delay,mode:mode,inputPath:inputPath,releasePath:releasePath}}); if err != nil { panic(err) }
return service
}
firstDelay := time.Duration(0); if mode == "lost-authority" { firstDelay = 300*time.Millisecond }
@@ -165,7 +177,8 @@ func main() {
done := make(chan error,len(services)); for _, current := range services { go func(server *gateway.Server){done <- server.Serve(ctx)}(current) }
keyDER, err := x509.MarshalPKCS8PrivateKey(clientKey); if err != nil { panic(err) }
admissionKeyDER, err := x509.MarshalPKCS8PrivateKey(admissionKey); if err != nil { panic(err) }
manifest := map[string]any{"version":"1","purpose":"launch","session_id":"session-1","reconnect_sequence":0,"gateway":map[string]any{"id":"gateway-1","addresses":addresses,"public_identity":"gateway.test"},"tunnel":map[string]any{"versions":[]string{"verse-gateway-v1/1"},"features":[]string{"control.v1"}},"profile":map[string]any{"id":"standard","bounds":map[string]any{"minimum_kbps":1000,"target_kbps":5000,"maximum_kbps":10000},"display_mode":map[string]any{"resolution_width":1920,"resolution_height":1080,"fps":60}},"grant":map[string]any{"opaque_value":"ggggggggggggggggggggggggggggggggggggggggggg","expires_at":"2099-01-01T00:00:00Z","audience":"versevdi-gateway"},"correlation_id":"oracle"}
purpose := "launch"; reconnectSequence := 0; if mode == "reconnect-success" { purpose = "reconnect"; reconnectSequence = 1 }
manifest := map[string]any{"version":"1","purpose":purpose,"session_id":"session-1","reconnect_sequence":reconnectSequence,"gateway":map[string]any{"id":"gateway-1","addresses":addresses,"public_identity":"gateway.test"},"tunnel":map[string]any{"versions":[]string{"verse-gateway-v1/1"},"features":[]string{"control.v1","input.absolute.v1","input.scroll.v1"}},"profile":map[string]any{"id":"standard","bounds":map[string]any{"minimum_kbps":1000,"target_kbps":5000,"maximum_kbps":10000},"display_mode":map[string]any{"resolution_width":1920,"resolution_height":1080,"fps":60}},"grant":map[string]any{"opaque_value":"ggggggggggggggggggggggggggggggggggggggggggg","expires_at":"2099-01-01T00:00:00Z","audience":"versevdi-gateway"},"correlation_id":"oracle"}
credential := map[string]any{"client_device_id":"device","device_key_id":"key","certificate_chain_pem":pemCert(clientDER,caDER),"trust_bundle_pem":pemCert(caDER),"expires_at":"2099-01-01T00:00:00Z"}
manifestJSON,_ := json.Marshal(manifest); credentialJSON,_ := json.Marshal(credential)
wrongKeyDER, err := x509.MarshalPKCS8PrivateKey(wrongClientKey); if err != nil { panic(err) }
@@ -178,27 +191,27 @@ func main() {
"#;
#[derive(Deserialize)]
struct Ready {
manifest: String,
credential: String,
admission_key: String,
client_key: String,
wrong_client_key: String,
server_key: String,
server_chain: String,
bad_trust: String,
replay_rejected: bool,
pub(crate) struct Ready {
pub(crate) manifest: String,
pub(crate) credential: String,
pub(crate) admission_key: String,
pub(crate) client_key: String,
pub(crate) wrong_client_key: String,
pub(crate) server_key: String,
pub(crate) server_chain: String,
pub(crate) bad_trust: String,
pub(crate) replay_rejected: bool,
}
struct Oracle {
pub(crate) struct Oracle {
_serial: std::sync::MutexGuard<'static, ()>,
child: Child,
directory: std::path::PathBuf,
ready: Ready,
pub(crate) ready: Ready,
}
impl Oracle {
fn start(mode: &str) -> Self {
pub(crate) fn start(mode: &str) -> Self {
static SERIAL: Mutex<()> = Mutex::new(());
static NEXT: AtomicUsize = AtomicUsize::new(1);
let serial = SERIAL
@@ -253,6 +266,27 @@ impl Oracle {
.parse()
.expect("provider mutation count")
}
fn input(&self) -> Option<Vec<u8>> {
fs::read(self.directory.join("ready.json.input")).ok()
}
fn release_all(&self) -> bool {
self.directory.join("ready.json.release-all").exists()
}
fn files(&self) -> Vec<String> {
fs::read_dir(&self.directory)
.expect("read oracle directory")
.map(|entry| {
entry
.expect("oracle entry")
.file_name()
.to_string_lossy()
.into_owned()
})
.collect()
}
}
impl Drop for Oracle {
@@ -263,7 +297,7 @@ impl Drop for Oracle {
}
}
fn test_key(encoded: &str) -> Arc<dyn SigningKey> {
pub(crate) fn test_key(encoded: &str) -> Arc<dyn SigningKey> {
let der = STANDARD.decode(encoded).expect("decode test key");
rustls::crypto::ring::default_provider()
.key_provider
@@ -339,6 +373,237 @@ fn callback_ed25519_signer_completes_tls13_quic_admission_without_private_key_in
runtime.block_on(session.close());
}
#[test]
fn production_gateway_session_delivers_bounded_media_control_and_ordered_input() {
let oracle = Oracle::start("input");
let runtime = tokio::runtime::Runtime::new().expect("runtime");
let session = runtime
.block_on(connect_oracle_async(
&oracle,
&oracle.ready.admission_key,
&oracle.ready.client_key,
|_| {},
|_| {},
))
.expect("production Go gateway admission");
let (command_tx, command_rx) = tokio::sync::mpsc::channel(64);
let (event_tx, _event_rx) = bounded_session_events();
let cancellation = Cancellation::new();
let running_cancellation = cancellation.clone();
let mut running = runtime.spawn(session.run(command_rx, event_tx, running_cancellation));
runtime
.block_on(command_tx.send(SessionCommand::Input(
b"VGI1\x01\x04\x01\x00\x00\x1e".to_vec(),
)))
.expect("bounded input");
runtime.block_on(async { tokio::time::sleep(Duration::from_millis(100)).await });
let input_deadline = Instant::now() + Duration::from_secs(2);
while oracle.input().is_none() && Instant::now() < input_deadline {
thread::sleep(Duration::from_millis(10));
}
let early = runtime
.block_on(async { tokio::time::timeout(Duration::from_millis(10), &mut running).await });
if let (None, Ok(completed)) = (oracle.input(), early) {
panic!(
"gateway did not receive input; session result={:?}",
completed.expect("session task")
);
}
assert!(
oracle.input().is_some(),
"gateway did not receive input: {:?}",
oracle.files()
);
cancellation.cancel();
assert_eq!(
runtime.block_on(running).expect("session task"),
Err(CoreError::Cancelled)
);
let release_deadline = Instant::now() + Duration::from_secs(2);
while !oracle.release_all() && Instant::now() < release_deadline {
thread::sleep(Duration::from_millis(10));
}
assert!(
oracle.release_all(),
"gateway did not release pressed input"
);
}
#[test]
fn production_gateway_session_delivers_bounded_media_and_control() {
let oracle = Oracle::start("io");
let runtime = tokio::runtime::Runtime::new().expect("runtime");
let session = runtime
.block_on(connect_oracle_async(
&oracle,
&oracle.ready.admission_key,
&oracle.ready.client_key,
|_| {},
|_| {},
))
.expect("production Go gateway admission");
let (_command_tx, command_rx) = tokio::sync::mpsc::channel(64);
let (event_tx, event_rx) = bounded_session_events();
let cancellation = Cancellation::new();
let running_cancellation = cancellation.clone();
let running = runtime.spawn(session.run(command_rx, event_tx, running_cancellation));
let mut media = Vec::new();
let mut control = Vec::new();
let deadline = Instant::now() + Duration::from_secs(5);
while (media.len() < 2 || control.is_empty()) && Instant::now() < deadline {
match event_rx.recv_timeout(Duration::from_millis(100)) {
Ok(SessionEvent::Media(unit)) => media.push(unit),
Ok(SessionEvent::Control { kind, payload }) => control.push((kind, payload)),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
Err(error) => panic!("session event channel: {error}"),
}
}
media.sort_by_key(|unit| unit.channel as u8);
assert_eq!(media.len(), 2);
assert!(media.iter().any(|unit| unit.payload == vec![0xA5; 2000]));
assert!(media
.iter()
.any(|unit| unit.payload == vec![0x10, 0x20, 0x30]));
assert_eq!(control, vec![(0x11, vec![0, 0, 1, 0, 2])]);
cancellation.cancel();
assert_eq!(
runtime.block_on(running).expect("session task"),
Err(CoreError::Cancelled)
);
}
#[repr(C)]
struct AbiCore {
_private: [u8; 0],
}
#[repr(C)]
#[derive(Clone, Copy)]
struct AbiBytes {
data: *const u8,
length: usize,
}
#[repr(C)]
struct AbiConfig {
struct_size: u32,
abi_version: u32,
context: *mut c_void,
sign_admission: Option<unsafe extern "C" fn(*mut c_void, AbiBytes, *mut u8) -> u32>,
sign_tls_ed25519: Option<unsafe extern "C" fn(*mut c_void, AbiBytes, *mut u8) -> u32>,
on_state: Option<unsafe extern "C" fn(*mut c_void, *const u8)>,
on_error: Option<unsafe extern "C" fn(*mut c_void, *const u8)>,
on_stats: Option<unsafe extern "C" fn(*mut c_void, *const u8)>,
on_media: Option<unsafe extern "C" fn(*mut c_void, *const u8)>,
on_control: Option<unsafe extern "C" fn(*mut c_void, *const u8)>,
}
#[repr(C)]
struct AbiConnect {
struct_size: u32,
abi_version: u32,
manifest_json: AbiBytes,
tunnel_credential_json: AbiBytes,
}
#[repr(C)]
struct AbiInput {
struct_size: u32,
abi_version: u32,
kind: u32,
flags: u32,
values: [i32; 12],
}
unsafe extern "C" {
fn verse_core_create_v1(config: *const AbiConfig, out: *mut *mut AbiCore) -> u32;
fn verse_core_connect_v1(core: *mut AbiCore, request: *const AbiConnect) -> u32;
fn verse_core_send_input_v1(core: *mut AbiCore, event: *const AbiInput) -> u32;
fn verse_core_cancel_v1(core: *mut AbiCore) -> u32;
fn verse_core_destroy_v1(core: *mut AbiCore, timeout_ms: u32) -> u32;
}
struct AbiSigners {
admission: Arc<dyn SigningKey>,
tls: Arc<dyn SigningKey>,
}
unsafe extern "C" fn abi_admission(raw: *mut c_void, input: AbiBytes, output: *mut u8) -> u32 {
unsafe { abi_sign(raw, input, output, true) }
}
unsafe extern "C" fn abi_tls(raw: *mut c_void, input: AbiBytes, output: *mut u8) -> u32 {
unsafe { abi_sign(raw, input, output, false) }
}
unsafe fn abi_sign(raw: *mut c_void, input: AbiBytes, output: *mut u8, admission: bool) -> u32 {
let context = unsafe { &*raw.cast::<AbiSigners>() };
let bytes = unsafe { std::slice::from_raw_parts(input.data, input.length) };
let key = if admission {
&context.admission
} else {
&context.tls
};
let signature = key
.choose_scheme(&[SignatureScheme::ED25519])
.expect("Ed25519")
.sign(bytes)
.expect("sign");
unsafe { ptr::copy_nonoverlapping(signature.as_ptr(), output, 64) };
0
}
#[test]
fn real_abi_exports_connect_send_cancel_and_destroy_the_go_gateway_session() {
let oracle = Oracle::start("input");
let mut signers = Box::new(AbiSigners {
admission: test_key(&oracle.ready.admission_key),
tls: test_key(&oracle.ready.client_key),
});
let config = AbiConfig {
struct_size: size_of::<AbiConfig>() as u32,
abi_version: 1,
context: ptr::from_mut(&mut *signers).cast(),
sign_admission: Some(abi_admission),
sign_tls_ed25519: Some(abi_tls),
on_state: None,
on_error: None,
on_stats: None,
on_media: None,
on_control: None,
};
let mut core = ptr::null_mut();
assert_eq!(unsafe { verse_core_create_v1(&config, &mut core) }, 0);
let request = AbiConnect {
struct_size: size_of::<AbiConnect>() as u32,
abi_version: 1,
manifest_json: AbiBytes {
data: oracle.ready.manifest.as_ptr(),
length: oracle.ready.manifest.len(),
},
tunnel_credential_json: AbiBytes {
data: oracle.ready.credential.as_ptr(),
length: oracle.ready.credential.len(),
},
};
assert_eq!(unsafe { verse_core_connect_v1(core, &request) }, 0);
let mut values = [0; 12];
values[0] = 1;
values[2] = 30;
let input = AbiInput {
struct_size: size_of::<AbiInput>() as u32,
abi_version: 1,
kind: 1,
flags: 0,
values,
};
assert_eq!(unsafe { verse_core_send_input_v1(core, &input) }, 0);
let deadline = Instant::now() + Duration::from_secs(2);
while oracle.input().is_none() && Instant::now() < deadline {
thread::sleep(Duration::from_millis(10));
}
assert!(oracle.input().is_some(), "ABI input did not reach gateway");
assert_eq!(unsafe { verse_core_cancel_v1(core) }, 0);
assert_eq!(unsafe { verse_core_destroy_v1(core, 2_000) }, 0);
assert!(
oracle.release_all(),
"destroy did not release provider input"
);
}
fn signer(key: Arc<dyn SigningKey>) -> impl Fn(&[u8]) -> Result<[u8; 64], CoreError> {
move |input| {
key.choose_scheme(&[SignatureScheme::ED25519])
@@ -356,6 +621,24 @@ fn connect_oracle(
tls_key: &str,
mutate_manifest: impl FnOnce(&mut serde_json::Value),
mutate_credential: impl FnOnce(&mut serde_json::Value),
) -> Result<versevdi_core::transport::TransportSession, CoreError> {
tokio::runtime::Runtime::new()
.expect("runtime")
.block_on(connect_oracle_async(
oracle,
admission_key,
tls_key,
mutate_manifest,
mutate_credential,
))
}
async fn connect_oracle_async(
oracle: &Oracle,
admission_key: &str,
tls_key: &str,
mutate_manifest: impl FnOnce(&mut serde_json::Value),
mutate_credential: impl FnOnce(&mut serde_json::Value),
) -> Result<versevdi_core::transport::TransportSession, CoreError> {
let mut manifest: serde_json::Value =
serde_json::from_str(&oracle.ready.manifest).expect("manifest JSON");
@@ -368,8 +651,7 @@ fn connect_oracle(
let credential = NativeTunnelCredential::decode(
&serde_json::to_vec(&credential).expect("encode credential"),
)?;
let runtime = tokio::runtime::Runtime::new().expect("runtime");
runtime.block_on(connect(
connect(
&manifest,
&credential,
Signers::new(
@@ -378,7 +660,8 @@ fn connect_oracle(
),
"2026-08-12T00:00:00Z",
Duration::from_secs(5),
))
)
.await
}
#[test]
@@ -465,6 +748,22 @@ fn production_gateway_and_rust_reject_authority_binding_and_capability_mismatche
}
}
#[test]
fn production_gateway_accepts_reconnect_only_when_exact_sequence_is_bound() {
let oracle = Oracle::start("reconnect-success");
let session = connect_oracle(
&oracle,
&oracle.ready.admission_key,
&oracle.ready.client_key,
|_| {},
|_| {},
)
.expect("exact reconnect sequence");
tokio::runtime::Runtime::new()
.expect("runtime")
.block_on(session.close());
}
#[test]
fn signer_purposes_are_not_interchangeable() {
let oracle = Oracle::start("");