feat(interpreter-data)!: allow only deterministic signature algorithms (#734)

Some public signature algorithms require a RNG, but it is not
available in certain environments like smartcontracts.
This commit is contained in:
Ivan Boldyrev 2023-11-02 18:43:35 +04:00 committed by GitHub
parent 55da7a64aa
commit 15ce40a1cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 243 additions and 35 deletions

4
Cargo.lock generated
View File

@ -144,6 +144,7 @@ dependencies = [
"bs58 0.5.0", "bs58 0.5.0",
"fluence-keypair", "fluence-keypair",
"serde", "serde",
"thiserror",
] ]
[[package]] [[package]]
@ -179,6 +180,7 @@ dependencies = [
"air-interpreter-cid", "air-interpreter-cid",
"air-interpreter-data", "air-interpreter-data",
"air-interpreter-interface", "air-interpreter-interface",
"air-interpreter-signatures",
"aquavm-air", "aquavm-air",
"avm-interface", "avm-interface",
"avm-server", "avm-server",
@ -202,6 +204,7 @@ dependencies = [
"air-interpreter-signatures", "air-interpreter-signatures",
"air-test-utils", "air-test-utils",
"aquavm-air-parser", "aquavm-air-parser",
"fluence-keypair",
"itertools", "itertools",
"maplit", "maplit",
"nom", "nom",
@ -341,6 +344,7 @@ dependencies = [
"air-utils", "air-utils",
"aquavm-air-parser", "aquavm-air-parser",
"borsh 0.10.3", "borsh 0.10.3",
"bs58 0.5.0",
"concat-idents", "concat-idents",
"criterion 0.3.6", "criterion 0.3.6",
"csv", "csv",

View File

@ -52,6 +52,7 @@ fluence-app-service = "0.29.0"
marine-rs-sdk = { version = "0.10.0", features = ["logger"] } marine-rs-sdk = { version = "0.10.0", features = ["logger"] }
borsh = "0.10.3" borsh = "0.10.3"
bs58 = "0.5.0"
# the feature just silence a warning in the criterion 0.3.x. # the feature just silence a warning in the criterion 0.3.x.
criterion = { version = "0.3.3", features = ["html_reports"] } criterion = { version = "0.3.3", features = ["html_reports"] }
csv = "1.1.5" csv = "1.1.5"

View File

@ -24,9 +24,9 @@ use crate::INTERPRETER_SUCCESS;
use air_interpreter_data::InterpreterData; use air_interpreter_data::InterpreterData;
use air_interpreter_interface::CallRequests; use air_interpreter_interface::CallRequests;
use air_interpreter_signatures::KeyPair;
use air_utils::measure; use air_utils::measure;
use fluence_keypair::error::SigningError; use fluence_keypair::error::SigningError;
use fluence_keypair::KeyPair;
use std::fmt::Debug; use std::fmt::Debug;
use std::hash::Hash; use std::hash::Hash;
@ -138,7 +138,7 @@ fn sign_result(exec_ctx: &mut ExecutionCtx<'_>, keypair: &KeyPair) -> Result<(),
.map_err(signing_error_into_outcome)?; .map_err(signing_error_into_outcome)?;
let current_pubkey = keypair.public(); let current_pubkey = keypair.public();
exec_ctx.signature_store.put(current_pubkey.into(), current_signature); exec_ctx.signature_store.put(current_pubkey, current_signature);
Ok(()) Ok(())
} }

View File

@ -81,11 +81,8 @@ pub enum PreparationError {
}, },
/// Malformed keypair format data. /// Malformed keypair format data.
#[error("malformed keypair format: {error:?}")] #[error("malformed keypair format: {0}")]
MalformedKeyPairData { MalformedKeyPairData(#[from] air_interpreter_signatures::KeyError),
#[from]
error: fluence_keypair::error::DecodingError,
},
/// Failed to verify CidStore contents of the current data. /// Failed to verify CidStore contents of the current data.
#[error(transparent)] #[error(transparent)]

View File

@ -21,10 +21,11 @@ use crate::execution_step::TraceHandler;
use air_interpreter_data::InterpreterData; use air_interpreter_data::InterpreterData;
use air_interpreter_interface::RunParameters; use air_interpreter_interface::RunParameters;
use air_interpreter_signatures::KeyError;
use air_interpreter_signatures::KeyPair;
use air_interpreter_signatures::SignatureStore; use air_interpreter_signatures::SignatureStore;
use air_parser::ast::Instruction; use air_parser::ast::Instruction;
use fluence_keypair::KeyFormat; use fluence_keypair::KeyFormat;
use fluence_keypair::KeyPair;
use std::convert::TryFrom; use std::convert::TryFrom;
@ -88,7 +89,7 @@ pub(crate) fn prepare<'i>(
)?; )?;
let trace_handler = TraceHandler::from_trace(prev_data.trace, current_data.trace); let trace_handler = TraceHandler::from_trace(prev_data.trace, current_data.trace);
let key_format = KeyFormat::try_from(run_parameters.key_format)?; let key_format = KeyFormat::try_from(run_parameters.key_format).map_err(KeyError::from)?;
let keypair = KeyPair::from_secret_key(run_parameters.secret_key_bytes, key_format)?; let keypair = KeyPair::from_secret_key(run_parameters.secret_key_bytes, key_format)?;
let result = PreparationDescriptor { let result = PreparationDescriptor {

View File

@ -16,7 +16,9 @@
use crate::ExecutionError; use crate::ExecutionError;
use air_interpreter_signatures::{PeerCidTracker, SignatureStore}; use air_interpreter_signatures::KeyPair;
use air_interpreter_signatures::PeerCidTracker;
use air_interpreter_signatures::SignatureStore;
#[cfg(feature = "gen_signatures")] #[cfg(feature = "gen_signatures")]
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
@ -24,7 +26,7 @@ pub(crate) fn sign_produced_cids(
signature_tracker: &mut PeerCidTracker, signature_tracker: &mut PeerCidTracker,
signature_store: &mut SignatureStore, signature_store: &mut SignatureStore,
salt: &str, salt: &str,
keypair: &fluence_keypair::KeyPair, keypair: &KeyPair,
) -> Result<(), ExecutionError> { ) -> Result<(), ExecutionError> {
use crate::UncatchableError; use crate::UncatchableError;
@ -42,7 +44,7 @@ pub(crate) fn sign_produced_cids(
_signature_tracker: &mut PeerCidTracker, _signature_tracker: &mut PeerCidTracker,
_signature_store: &mut SignatureStore, _signature_store: &mut SignatureStore,
_salt: &str, _salt: &str,
_keypair: &fluence_keypair::KeyPair, _keypair: &KeyPair,
) -> Result<(), ExecutionError> { ) -> Result<(), ExecutionError> {
Ok(()) Ok(())
} }

View File

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
#[cfg(feature = "check_signatures")]
mod algorithms;
#[cfg(feature = "check_signatures")] #[cfg(feature = "check_signatures")]
mod attacks; mod attacks;
#[cfg(feature = "check_signatures")] #[cfg(feature = "check_signatures")]

View File

@ -0,0 +1,95 @@
/*
* Copyright 2023 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use air::{min_supported_version, PreparationError};
use air_interpreter_data::{verification::DataVerifierError, InterpreterData};
use air_interpreter_signatures::KeyError;
use air_test_utils::{
assert_error_eq,
prelude::{request_sent_by, unit_call_service},
test_runner::{create_avm, create_avm_with_key, NativeAirRunner, TestRunParameters},
};
use fluence_keypair::KeyFormat;
use serde_json::json;
/// Checking that other peers' key algorithms are valid.
#[test]
fn test_banned_signature() {
let air_script = r#"(call "other_peer_id" ("" "") [])"#;
let bad_algo_keypair = fluence_keypair::KeyPair::generate_secp256k1();
let bad_algo_pk = bad_algo_keypair.public();
let bad_algo_signature: air_interpreter_signatures::Signature =
air_interpreter_signatures::sign_cids(vec![], "particle_id", &bad_algo_keypair)
.unwrap()
.into();
let bad_algo_pk_ser = bs58::encode(bad_algo_pk.encode()).into_string();
let bad_signature_store = json!({
bad_algo_pk_ser: bad_algo_signature,
});
let bad_peer_id = bad_algo_pk.to_peer_id().to_string();
let trace = vec![request_sent_by("init_peer_fake_id")];
let mut data = serde_json::to_value(InterpreterData::from_execution_result(
trace.into(),
<_>::default(),
<_>::default(),
<_>::default(),
min_supported_version().clone(),
))
.unwrap();
data["signatures"] = bad_signature_store;
let current_data = data.to_string();
let mut avm = create_avm(unit_call_service(), "other_peer_id");
let res = avm
.call(
air_script,
"",
current_data,
TestRunParameters::from_init_peer_id("init_peer_fake_id"),
)
.unwrap();
assert_error_eq!(
&res,
PreparationError::DataSignatureCheckError(DataVerifierError::MalformedKey {
error: KeyError::AlgorithmNotWhitelisted(KeyFormat::Secp256k1),
peer_id: bad_peer_id
})
);
}
/// Checking that local key is valid.
#[test]
fn test_banned_signing_key() {
let air_script = "(null)";
let bad_algo_keypair = fluence_keypair::KeyPair::generate_secp256k1();
let mut avm = create_avm_with_key::<NativeAirRunner>(bad_algo_keypair, unit_call_service());
let res = avm
.call(air_script, "", "", TestRunParameters::from_init_peer_id("init_peer_id"))
.unwrap();
assert_error_eq!(
&res,
PreparationError::MalformedKeyPairData(KeyError::AlgorithmNotWhitelisted(KeyFormat::Secp256k1))
);
}

View File

@ -46,7 +46,7 @@ fn issue_310() {
0, 0,
None, None,
call_results, call_results,
&key_pair, key_pair.as_inner(),
particle_id.to_owned(), particle_id.to_owned(),
) )
.unwrap() .unwrap()

View File

@ -17,11 +17,12 @@
use std::rc::Rc; use std::rc::Rc;
use air_interpreter_cid::CidRef; use air_interpreter_cid::CidRef;
use air_interpreter_signatures::KeyError;
use thiserror::Error as ThisError; use thiserror::Error as ThisError;
#[derive(Debug, ThisError)] #[derive(Debug, ThisError)]
pub enum DataVerifierError { pub enum DataVerifierError {
#[error(transparent)] #[error("malformed key at peer: {peer_id:?}: {error}")]
MalformedKey(fluence_keypair::error::DecodingError), MalformedKey { error: KeyError, peer_id: String },
#[error(transparent)] #[error(transparent)]
MalformedSignature(fluence_keypair::error::DecodingError), MalformedSignature(fluence_keypair::error::DecodingError),

View File

@ -42,6 +42,16 @@ impl<'data> DataVerifier<'data> {
// it can be further optimized if only required parts are passed // it can be further optimized if only required parts are passed
// SignatureStore is not used elsewhere // SignatureStore is not used elsewhere
pub fn new(data: &'data InterpreterData, salt: &'data str) -> Result<Self, DataVerifierError> { pub fn new(data: &'data InterpreterData, salt: &'data str) -> Result<Self, DataVerifierError> {
// validate key algoritms
for (public_key, _) in data.signatures.iter() {
public_key
.validate()
.map_err(|error| DataVerifierError::MalformedKey {
error,
peer_id: public_key.to_peer_id(),
})?;
}
// it contains signature too; if we try to add a value to a peer w/o signature, it is an immediate error // it contains signature too; if we try to add a value to a peer w/o signature, it is an immediate error
let mut grouped_cids: HashMap<Box<str>, PeerInfo<'data>> = data let mut grouped_cids: HashMap<Box<str>, PeerInfo<'data>> = data
.signatures .signatures

View File

@ -18,3 +18,8 @@ bs58 = "0.5.0"
borsh = { version = "0.10.3", features = ["rc"]} borsh = { version = "0.10.3", features = ["rc"]}
borsh-derive = "0.10.3" borsh-derive = "0.10.3"
serde = { version = "1.0.190", features = ["derive"] } serde = { version = "1.0.190", features = ["derive"] }
thiserror = "1.0.49"
[features]
default = ["rand"]
rand = []

View File

@ -33,14 +33,24 @@ mod trackers;
pub use crate::stores::*; pub use crate::stores::*;
pub use crate::trackers::*; pub use crate::trackers::*;
pub use fluence_keypair::KeyPair; pub use fluence_keypair::KeyFormat;
use borsh::BorshSerialize; use borsh::BorshSerialize;
use fluence_keypair::error::SigningError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::hash::Hash; use std::hash::Hash;
use std::ops::Deref; use std::ops::Deref;
#[derive(thiserror::Error, Debug)]
pub enum KeyError {
#[error("signature algorithm {0:?} not whitelisted")]
AlgorithmNotWhitelisted(fluence_keypair::KeyFormat),
#[error("invalid key data: {0}")]
InvalidKeyData(#[from] fluence_keypair::error::DecodingError),
}
/// An opaque serializable representation of a public key. /// An opaque serializable representation of a public key.
/// ///
/// It can be a string or a binary, you shouldn't care about it unless you change serialization format. /// It can be a string or a binary, you shouldn't care about it unless you change serialization format.
@ -55,6 +65,10 @@ pub struct PublicKey(
); );
impl PublicKey { impl PublicKey {
pub fn new(inner: fluence_keypair::PublicKey) -> Self {
Self(inner)
}
pub fn verify<T: BorshSerialize + ?Sized>( pub fn verify<T: BorshSerialize + ?Sized>(
&self, &self,
value: &T, value: &T,
@ -66,6 +80,15 @@ impl PublicKey {
let serialized_value = SaltedData::new(&value, salt).serialize(); let serialized_value = SaltedData::new(&value, salt).serialize();
pk.verify(&serialized_value, signature) pk.verify(&serialized_value, signature)
} }
pub fn to_peer_id(&self) -> String {
self.0.to_peer_id().to_string()
}
pub fn validate(&self) -> Result<(), KeyError> {
let key_format = self.get_key_format();
validate_with_key_format((), key_format)
}
} }
impl Deref for PublicKey { impl Deref for PublicKey {
@ -82,9 +105,72 @@ impl Hash for PublicKey {
} }
} }
impl From<fluence_keypair::PublicKey> for PublicKey { #[derive(Clone)]
fn from(value: fluence_keypair::PublicKey) -> Self { pub struct KeyPair(fluence_keypair::KeyPair);
Self(value)
impl KeyPair {
pub fn new(inner: fluence_keypair::KeyPair) -> Result<Self, KeyError> {
let key_format = inner.key_format();
validate_with_key_format((), key_format)?;
Ok(Self(inner))
}
pub fn from_secret_key(secret_key: Vec<u8>, key_format: KeyFormat) -> Result<Self, KeyError> {
let inner = fluence_keypair::KeyPair::from_secret_key(secret_key, key_format)?;
Self::new(inner)
}
pub fn public(&self) -> PublicKey {
PublicKey::new(self.0.public())
}
pub fn key_format(&self) -> KeyFormat {
self.0.key_format()
}
pub fn sign(&self, msg: &[u8]) -> Result<Signature, SigningError> {
self.0.sign(msg).map(Signature::new)
}
pub fn secret(&self) -> Vec<u8> {
self.0.secret().expect("cannot fail on supported formats")
}
pub fn into_inner(self) -> fluence_keypair::KeyPair {
self.0
}
pub fn as_inner(&self) -> &fluence_keypair::KeyPair {
&self.0
}
#[cfg(feature = "rand")]
pub fn generate(key_format: KeyFormat) -> Result<Self, KeyError> {
validate_with_key_format((), key_format)?;
Ok(Self(fluence_keypair::KeyPair::generate(key_format)))
}
}
impl TryFrom<fluence_keypair::KeyPair> for KeyPair {
type Error = KeyError;
fn try_from(value: fluence_keypair::KeyPair) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<KeyPair> for fluence_keypair::KeyPair {
fn from(value: KeyPair) -> Self {
value.0
}
}
pub(crate) fn validate_with_key_format<V>(inner: V, key_format: KeyFormat) -> Result<V, KeyError> {
match key_format {
fluence_keypair::KeyFormat::Ed25519 => Ok(inner),
_ => Err(KeyError::AlgorithmNotWhitelisted(key_format)),
} }
} }

View File

@ -34,7 +34,7 @@ impl serde::de::Visitor<'_> for PublicKeyVisitor {
type Value = fluence_keypair::PublicKey; type Value = fluence_keypair::PublicKey;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("expecting a base58-encoded public key string") formatter.write_str("a base58-encoded public key string")
} }
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
@ -43,9 +43,8 @@ impl serde::de::Visitor<'_> for PublicKeyVisitor {
{ {
use serde::de; use serde::de;
let public_key = fluence_keypair::PublicKey::from_base58(v) fluence_keypair::PublicKey::from_base58(v)
.map_err(|_| de::Error::invalid_value(de::Unexpected::Str(v), &self))?; .map_err(|_| de::Error::invalid_value(de::Unexpected::Str(v), &self))
Ok(public_key)
} }
} }

View File

@ -14,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
use crate::KeyPair;
use crate::SaltedData; use crate::SaltedData;
use air_interpreter_cid::{CidRef, CID}; use air_interpreter_cid::{CidRef, CID};
use fluence_keypair::error::SigningError; use fluence_keypair::error::SigningError;
use fluence_keypair::KeyPair;
use std::rc::Rc; use std::rc::Rc;
@ -48,17 +48,17 @@ impl PeerCidTracker {
salt: &str, salt: &str,
keypair: &KeyPair, keypair: &KeyPair,
) -> Result<crate::Signature, SigningError> { ) -> Result<crate::Signature, SigningError> {
sign_cids(self.cids.clone(), salt, keypair) sign_cids(self.cids.clone(), salt, &keypair.0).map(Into::into)
} }
} }
fn sign_cids( pub fn sign_cids(
mut cids: Vec<Rc<CidRef>>, mut cids: Vec<Rc<CidRef>>,
salt: &str, salt: &str,
keypair: &KeyPair, keypair: &fluence_keypair::KeyPair,
) -> Result<crate::Signature, SigningError> { ) -> Result<fluence_keypair::Signature, SigningError> {
cids.sort_unstable(); cids.sort_unstable();
let serialized_cids = SaltedData::new(&cids, salt).serialize(); let serialized_cids = SaltedData::new(&cids, salt).serialize();
keypair.sign(&serialized_cids).map(crate::Signature::new) keypair.sign(&serialized_cids)
} }

View File

@ -19,6 +19,7 @@ aquavm-air = { version = "0.54.0", path = "../../../air" }
air-interpreter-cid = { version = "0.6.0", path = "../interpreter-cid" } air-interpreter-cid = { version = "0.6.0", path = "../interpreter-cid" }
air-interpreter-data = { version = "0.14.0", path = "../interpreter-data" } air-interpreter-data = { version = "0.14.0", path = "../interpreter-data" }
air-interpreter-interface = { version = "0.15.1", path = "../interpreter-interface" } air-interpreter-interface = { version = "0.15.1", path = "../interpreter-interface" }
air-interpreter-signatures = { version = "0.1.3", path = "../interpreter-signatures" }
avm-interface = { version = "0.29.2", path = "../../../avm/interface" } avm-interface = { version = "0.29.2", path = "../../../avm/interface" }
avm-server = { version = "0.33.3", path = "../../../avm/server" } avm-server = { version = "0.33.3", path = "../../../avm/server" }
marine-rs-sdk = "0.10.0" marine-rs-sdk = "0.10.0"

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
use fluence_keypair::KeyPair; use air_interpreter_signatures::KeyPair;
use rand_chacha::rand_core::SeedableRng; use rand_chacha::rand_core::SeedableRng;
/// Derive fake keypair for testing proposes. /// Derive fake keypair for testing proposes.
@ -25,6 +25,7 @@ use rand_chacha::rand_core::SeedableRng;
// Should be moved to test lib when keypair interface PR is merged. // Should be moved to test lib when keypair interface PR is merged.
pub fn derive_dummy_keypair(seed: &str) -> (KeyPair, String) { pub fn derive_dummy_keypair(seed: &str) -> (KeyPair, String) {
use sha2::{Digest as _, Sha256}; use sha2::{Digest as _, Sha256};
use std::convert::TryFrom;
let mut rng = { let mut rng = {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
@ -33,7 +34,8 @@ pub fn derive_dummy_keypair(seed: &str) -> (KeyPair, String) {
}; };
let keypair_ed25519 = ed25519_dalek::Keypair::generate(&mut rng); let keypair_ed25519 = ed25519_dalek::Keypair::generate(&mut rng);
let keypair: KeyPair = KeyPair::Ed25519(keypair_ed25519.into()); let keypair = fluence_keypair::KeyPair::Ed25519(keypair_ed25519.into());
let keypair = KeyPair::try_from(keypair).expect("cannot happen");
let peer_id = keypair.public().to_peer_id().to_string(); let peer_id = keypair.public().to_peer_id().to_string();
(keypair, peer_id) (keypair, peer_id)

View File

@ -177,14 +177,15 @@ pub fn create_custom_avm<R: AirRunner>(
TestRunner { TestRunner {
runner, runner,
call_service, call_service,
keypair, keypair: keypair.into_inner(),
} }
} }
pub fn create_avm_with_key<R: AirRunner>( pub fn create_avm_with_key<R: AirRunner>(
keypair: KeyPair, keypair: impl Into<KeyPair>,
call_service: CallServiceClosure, call_service: CallServiceClosure,
) -> TestRunner<R> { ) -> TestRunner<R> {
let keypair = keypair.into();
let current_peer_id = keypair.public().to_peer_id().to_string(); let current_peer_id = keypair.public().to_peer_id().to_string();
let runner = R::new(current_peer_id); let runner = R::new(current_peer_id);

View File

@ -18,6 +18,7 @@ air-test-utils = { version = "0.12.1", path = "../air-lib/test-utils" }
aquavm-air-parser = { version = "0.10.0", path = "../air-lib/air-parser" } aquavm-air-parser = { version = "0.10.0", path = "../air-lib/air-parser" }
itertools = "0.10.5" itertools = "0.10.5"
fluence-keypair = "0.10.1"
strum = { version="0.24.1", features=["derive"] } strum = { version="0.24.1", features=["derive"] }
nom = "7.1.3" nom = "7.1.3"
nom_locate = "4.1.0" nom_locate = "4.1.0"

View File

@ -22,7 +22,6 @@ use crate::{
services::{services_to_call_service_closure, MarineServiceHandle, NetworkServices}, services::{services_to_call_service_closure, MarineServiceHandle, NetworkServices},
}; };
use air_interpreter_signatures::KeyPair;
use air_test_utils::{ use air_test_utils::{
key_utils::derive_dummy_keypair, key_utils::derive_dummy_keypair,
test_runner::{ test_runner::{
@ -30,6 +29,7 @@ use air_test_utils::{
}, },
RawAVMOutcome, RawAVMOutcome,
}; };
use fluence_keypair::KeyPair;
use std::{borrow::Borrow, cell::RefCell, collections::HashMap, hash::Hash, ops::Deref, rc::Rc}; use std::{borrow::Borrow, cell::RefCell, collections::HashMap, hash::Hash, ops::Deref, rc::Rc};
@ -86,7 +86,7 @@ pub struct Peer<R> {
} }
impl<R: AirRunner> Peer<R> { impl<R: AirRunner> Peer<R> {
pub fn new(keypair: KeyPair, services: Rc<[MarineServiceHandle]>) -> Self { pub fn new(keypair: impl Into<KeyPair>, services: Rc<[MarineServiceHandle]>) -> Self {
let call_service = services_to_call_service_closure(services); let call_service = services_to_call_service_closure(services);
let runner = create_avm_with_key::<R>(keypair, call_service); let runner = create_avm_with_key::<R>(keypair, call_service);