Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions linera-execution/src/execution_state_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,8 @@ where
.execution_runtime_config()
.allow_application_logs;

let transaction_index = self.txn_tracker.transaction_index();

let contract_runtime_task = self
.state
.context()
Expand All @@ -1016,6 +1018,7 @@ where
let runtime = ContractSyncRuntime::new(
execution_state_sender,
chain_id,
transaction_index,
refund_grant_to,
controller,
&action,
Expand Down
3 changes: 3 additions & 0 deletions linera-execution/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,9 @@ pub trait ContractRuntime: BaseRuntime {
/// The authenticated owner for this execution, if there is one.
fn authenticated_owner(&mut self) -> Result<Option<AccountOwner>, ExecutionError>;

/// The index of the current transaction within its block.
fn transaction_index(&mut self) -> Result<u32, ExecutionError>;

/// If the current message (if there is one) was rejected by its destination and is now
/// bouncing back.
fn message_is_bouncing(&mut self) -> Result<Option<bool>, ExecutionError>;
Expand Down
11 changes: 11 additions & 0 deletions linera-execution/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pub struct SyncRuntimeInternal<UserInstance: WithContext> {
/// The height of the next block that will be added to this chain. During operations
/// and messages, this is the current block height.
height: BlockHeight,
/// The index of the current transaction within its block.
transaction_index: u32,
/// The current consensus round. Only available during block validation in multi-leader rounds.
round: Option<u32>,
/// The current message being executed, if there is one.
Expand Down Expand Up @@ -313,6 +315,7 @@ impl<UserInstance: WithContext> SyncRuntimeInternal<UserInstance> {
fn new(
chain_id: ChainId,
height: BlockHeight,
transaction_index: u32,
round: Option<u32>,
executing_message: Option<ExecutingMessage>,
execution_state_sender: ExecutionStateSender,
Expand All @@ -325,6 +328,7 @@ impl<UserInstance: WithContext> SyncRuntimeInternal<UserInstance> {
Self {
chain_id,
height,
transaction_index,
round,
executing_message,
execution_state_sender,
Expand Down Expand Up @@ -1045,6 +1049,7 @@ impl ContractSyncRuntime {
pub(crate) fn new(
execution_state_sender: ExecutionStateSender,
chain_id: ChainId,
transaction_index: u32,
refund_grant_to: Option<Account>,
resource_controller: ResourceController,
action: &UserAction,
Expand All @@ -1054,6 +1059,7 @@ impl ContractSyncRuntime {
SyncRuntimeInternal::new(
chain_id,
action.height(),
transaction_index,
action.round(),
if let UserAction::Message(context, _) = action {
Some(context.into())
Expand Down Expand Up @@ -1214,6 +1220,10 @@ impl ContractRuntime for ContractSyncRuntimeHandle {
Ok(this.current_application().signer)
}

fn transaction_index(&mut self) -> Result<u32, ExecutionError> {
Ok(self.inner().transaction_index)
}

fn message_is_bouncing(&mut self) -> Result<Option<bool>, ExecutionError> {
Ok(self
.inner()
Expand Down Expand Up @@ -1747,6 +1757,7 @@ impl ServiceSyncRuntime {
SyncRuntimeInternal::new(
context.chain_id,
context.next_block_height,
0,
None,
None,
execution_state_sender,
Expand Down
1 change: 1 addition & 0 deletions linera-execution/src/unit_tests/runtime_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ where
let runtime = SyncRuntimeInternal::new(
chain_id,
BlockHeight(0),
0,
Some(0),
None,
execution_state_sender,
Expand Down
9 changes: 9 additions & 0 deletions linera-execution/src/wasm/runtime_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,15 @@ where
.map_err(|error| RuntimeError::Custom(error.into()))
}

/// Returns the index of the current transaction within its block.
fn transaction_index(caller: &mut Caller) -> Result<u32, RuntimeError> {
caller
.user_data_mut()
.runtime
.transaction_index()
.map_err(|error| RuntimeError::Custom(error.into()))
}

/// Returns `Some(true)` if the incoming message was rejected from the original destination and
/// is now bouncing back, `Some(false)` if the message is being currently being delivered to
/// its original destination, or [`None`] if not executing an incoming message.
Expand Down
9 changes: 9 additions & 0 deletions linera-sdk/src/contract/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ where
contract_wit::authenticated_owner().map(AccountOwner::from)
}

/// Returns the index of the current transaction within its block.
///
/// Combined with the chain ID, application ID, and block height, this can be used by
/// smart contracts to derive a deterministic seed (e.g. for pseudo-random number
/// generation).
pub fn transaction_index(&mut self) -> u32 {
contract_wit::transaction_index()
}

/// Returns [`true`] if the incoming message was rejected from the original destination and is
/// now bouncing back, or [`None`] if not executing an incoming message.
pub fn message_is_bouncing(&mut self) -> Option<bool> {
Expand Down
22 changes: 22 additions & 0 deletions linera-sdk/src/contract/test_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ where
application_descriptions: HashMap<ApplicationId, ApplicationDescription>,
chain_id: Option<ChainId>,
authenticated_owner: Option<Option<AccountOwner>>,
transaction_index: Option<u32>,
block_height: Option<BlockHeight>,
round: Option<u32>,
message_is_bouncing: Option<Option<bool>>,
Expand Down Expand Up @@ -110,6 +111,7 @@ where
application_descriptions: HashMap::new(),
chain_id: None,
authenticated_owner: None,
transaction_index: None,
block_height: None,
round: None,
message_is_bouncing: None,
Expand Down Expand Up @@ -305,6 +307,26 @@ where
)
}

/// Configures the transaction index to return during the test.
pub fn with_transaction_index(mut self, transaction_index: u32) -> Self {
self.transaction_index = Some(transaction_index);
self
}

/// Configures the transaction index to return during the test.
pub fn set_transaction_index(&mut self, transaction_index: u32) -> &mut Self {
self.transaction_index = Some(transaction_index);
self
}

/// Returns the index of the current transaction within its block.
pub fn transaction_index(&mut self) -> u32 {
self.transaction_index.expect(
"Transaction index has not been mocked, \
please call `MockContractRuntime::set_transaction_index` first",
)
}

/// Configures the block height to return during the test.
pub fn with_block_height(mut self, block_height: BlockHeight) -> Self {
self.block_height = Some(block_height);
Expand Down
10 changes: 10 additions & 0 deletions linera-sdk/tests/fixtures/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions linera-sdk/tests/fixtures/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"event-subscriber",
"meta-counter",
"publish-read-data-blob",
"random-source",
"time-expiry",
"track-instantiation",
]
Expand All @@ -16,6 +17,7 @@ members = [
async-graphql = { version = "=7.0.17", default-features = false }
linera-sdk = { path = "../../../linera-sdk" }
log = "0.4.20"
rand = { version = "0.8.5", default-features = false, features = ["std_rng"] }
serde = { version = "1.0.152", features = ["derive"] }
tokio = { version = "1.25.0", features = ["macros", "rt-multi-thread"] }

Expand All @@ -27,6 +29,7 @@ create-and-call = { path = "./create-and-call" }
event-emitter = { path = "./event-emitter" }
meta-counter = { path = "./meta-counter" }
publish-read-data-blob = { path = "./publish-read-data-blob" }
random-source = { path = "./random-source" }
time-expiry = { path = "./time-expiry" }
track-instantiation = { path = "./track-instantiation" }

Expand Down
22 changes: 22 additions & 0 deletions linera-sdk/tests/fixtures/random-source/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "random-source"
version = "0.1.0"
authors = ["Linera <contact@linera.io>"]
edition = "2021"

[dependencies]
linera-sdk.workspace = true
rand.workspace = true
serde.workspace = true

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
linera-sdk = { workspace = true, features = ["test", "wasmer"] }
tokio.workspace = true

[[bin]]
name = "random_source_contract"
path = "src/contract.rs"

[[bin]]
name = "random_source_service"
path = "src/service.rs"
110 changes: 110 additions & 0 deletions linera-sdk/tests/fixtures/random-source/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) Zefchain Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

#![cfg_attr(target_arch = "wasm32", no_main)]

mod state;

use linera_sdk::{
linera_base_types::{
ApplicationId, BcsHashable, BlockHeight, ChainId, CryptoHash, WithContractAbi,
},
views::{RootView, View},
Contract, ContractRuntime,
};
use rand::{rngs::StdRng, RngCore, SeedableRng};
use random_source::RandomSourceAbi;
use serde::{Deserialize, Serialize};

use self::state::RandomSourceState;

pub struct RandomSourceContract {
state: RandomSourceState,
runtime: ContractRuntime<Self>,
}

linera_sdk::contract!(RandomSourceContract);

impl WithContractAbi for RandomSourceContract {
type Abi = RandomSourceAbi;
}

/// Inputs combined into the seed for the deterministic RNG. Only the
/// `transaction_index` makes the seed unique across consecutive operations
/// within the same block; the other fields make it unique across applications,
/// chains, and blocks.
#[derive(Serialize, Deserialize)]
struct RandomSourceSeed {
chain_id: ChainId,
application_id: ApplicationId,
block_height: BlockHeight,
transaction_index: u32,
}

impl BcsHashable<'_> for RandomSourceSeed {}

impl Contract for RandomSourceContract {
type Message = ();
type InstantiationArgument = ();
type Parameters = ();
type EventValue = ();

async fn load(runtime: ContractRuntime<Self>) -> Self {
let state = RandomSourceState::load(runtime.root_view_storage_context())
.await
.expect("Failed to load state");
RandomSourceContract { state, runtime }
}

async fn instantiate(&mut self, _argument: ()) {}

async fn execute_operation(&mut self, _operation: ()) {
// Asserting that the operation runs as the first transaction in the
// block exercises the fix for issue #2411: previously, the operation
// would have been preceded by an implicit system transaction.
let transaction_index = self.runtime.transaction_index();
assert_eq!(
transaction_index, 0,
"Expected operation to be the first transaction in the block, \
got transaction index {transaction_index}",
);

let seed_input = RandomSourceSeed {
chain_id: self.runtime.chain_id(),
application_id: self.runtime.application_id().forget_abi(),
block_height: self.runtime.block_height(),
transaction_index,
};
let hash_bytes = CryptoHash::new(&seed_input).as_bytes().0;
let seed = u64::from_le_bytes([
hash_bytes[0],
hash_bytes[1],
hash_bytes[2],
hash_bytes[3],
hash_bytes[4],
hash_bytes[5],
hash_bytes[6],
hash_bytes[7],
]);

let mut rng = StdRng::seed_from_u64(seed);
let sample1 = rng.next_u64();
let sample2 = rng.next_u64();
assert_ne!(
sample1, sample2,
"Two consecutive samples from the same RNG must differ",
);

self.state.seed.set(seed);
self.state.sample1.set(sample1);
self.state.sample2.set(sample2);
}

async fn execute_message(&mut self, _message: ()) {
panic!("RandomSource application doesn't support cross-chain messages");
}

async fn store(mut self) {
self.state.save().await.expect("Failed to save state");
}
}
43 changes: 43 additions & 0 deletions linera-sdk/tests/fixtures/random-source/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Zefchain Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

/*! ABI of the Random Source Test Application.

This fixture demonstrates how a smart contract can derive a deterministic
pseudo-random number from the runtime's `transaction_index`, combined with the
chain ID, application ID and block height. It also asserts that the operation
runs as the first transaction in the block, which exercises the fix for
<https://github.com/linera-io/linera-protocol/issues/2411>. */

use linera_sdk::linera_base_types::{ContractAbi, ServiceAbi};
use serde::{Deserialize, Serialize};

pub struct RandomSourceAbi;

impl ContractAbi for RandomSourceAbi {
type Operation = ();
type Response = ();
}

impl ServiceAbi for RandomSourceAbi {
type Query = Query;
type QueryResponse = QueryResponse;
}

/// Query exposed by the random source service.
#[derive(Debug, Deserialize, Serialize)]
pub enum Query {
/// Returns the seed and the random samples that the contract derived from it.
GetSamples,
}

/// Response from the random source service.
#[derive(Debug, Deserialize, Serialize)]
pub enum QueryResponse {
/// The seed and the two distinct random `u64` values it produced.
Samples {
seed: u64,
sample1: u64,
sample2: u64,
},
}
Loading
Loading