Skip to content

Commit 4862a00

Browse files
authored
Merge pull request #383 from OffchainLabs/contract-size
Contract size
2 parents 109b804 + 19c37ae commit 4862a00

File tree

22 files changed

+816
-266
lines changed

22 files changed

+816
-266
lines changed

cargo-stylus/src/commands/deploy.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::{
1212
};
1313
use alloy::primitives::{utils::parse_ether, Address, B256, U256};
1414
use eyre::eyre;
15-
use stylus_tools::core::deployment::deploy_wasm_file;
15+
use stylus_tools::core::deployment;
1616
use stylus_tools::core::deployment::deployer::ADDRESS;
1717

1818
pub const STYLUS_DEPLOYER_ADDRESS: Address = ADDRESS;
@@ -87,18 +87,28 @@ pub async fn exec(args: Args) -> CargoStylusResult {
8787
&args.build,
8888
&args.check,
8989
args.auth.get_max_fee_per_gas_wei()?,
90-
args.estimate_gas,
9190
args.no_activate,
9291
args.deployer_address,
9392
args.constructor_args,
9493
args.deployer_salt,
9594
args.constructor_value,
9695
);
97-
if let Some(wasm_file) = args.wasm_file {
98-
deploy_wasm_file(wasm_file, &config, &provider).await?;
96+
#[allow(clippy::collapsible_else_if)]
97+
if args.estimate_gas {
98+
if let Some(wasm_file) = args.wasm_file {
99+
let _gas = deployment::estimate_gas_wasm_file(wasm_file, &config, &provider).await?;
100+
} else {
101+
for contract in contracts {
102+
let _gas = deployment::estimate_gas(&contract, &config, &provider).await?;
103+
}
104+
}
99105
} else {
100-
for contract in contracts {
101-
contract.deploy(&config, &provider).await?;
106+
if let Some(wasm_file) = args.wasm_file {
107+
deployment::deploy_wasm_file(wasm_file, &config, &provider).await?;
108+
} else {
109+
for contract in contracts {
110+
contract.deploy(&config, &provider).await?;
111+
}
102112
}
103113
}
104114
Ok(())

cargo-stylus/src/commands/get_initcode.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,16 @@ pub fn exec(args: Args) -> CargoStylusResult {
3131
None => &mut io::stdout(),
3232
};
3333
let build_config = args.build.config();
34+
let chain_config = Default::default();
3435
let project_config = args.project.config();
3536
for contract in args.project.contracts()? {
36-
ops::write_initcode(&contract, &build_config, &project_config, &mut *writer)?;
37+
ops::write_initcode(
38+
&contract,
39+
&build_config,
40+
&chain_config,
41+
&project_config,
42+
&mut *writer,
43+
)?;
3744
}
3845
Ok(())
3946
}

cargo-stylus/src/common_args.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ impl DeployArgs {
151151
build: &BuildArgs,
152152
check: &CheckArgs,
153153
max_fee_per_gas_gwei: Option<u128>,
154-
estimate_gas: bool,
155154
no_activate: bool,
156155
deployer_address: Address,
157156
constructor_args: Vec<String>,
@@ -161,7 +160,6 @@ impl DeployArgs {
161160
DeploymentConfig {
162161
check: check.config(activate, build),
163162
max_fee_per_gas_gwei,
164-
estimate_gas,
165163
no_activate,
166164
deployer_address,
167165
constructor_args,

stylus-tools/precompiles/ArbOwner.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,12 @@ interface ArbOwner {
209209
bool enable
210210
) external;
211211

212+
/// @notice Sets the max amount of stylus contract fragments that can be used to deploy a stylus contract
213+
/// @notice Available in ArbOS version 60 and above
214+
function setMaxStylusContractFragments(
215+
uint8 maxFragments
216+
) external;
217+
212218
/// Emitted when a successful call is made to this precompile
213219
event OwnerActs(bytes4 indexed method, address indexed owner, bytes data);
214220
}

stylus-tools/precompiles/ArbOwnerPublic.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,9 @@ interface ArbOwnerPublic {
4444
/// Available in ArbOS version 40 with default as false
4545
function isCalldataPriceIncreaseEnabled() external view returns (bool);
4646

47+
/// @notice Get the max amount of stylus contract fragments that can be used to deploy a stylus contract
48+
/// @notice Available in ArbOS version 60 and above
49+
function getMaxStylusContractFragments() external view returns (uint8);
50+
4751
event ChainOwnerRectified(address rectifiedOwner);
4852
}

stylus-tools/src/core/activation.rs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//! details on contract activation.
88
99
use alloy::{
10-
primitives::{utils::parse_ether, Address, Bytes, U256},
10+
primitives::{utils::parse_ether, Address, U256},
1111
providers::{Provider, WalletProvider},
1212
rpc::types::{
1313
state::{AccountOverride, StateOverride},
@@ -16,6 +16,7 @@ use alloy::{
1616
};
1717

1818
use crate::{
19+
core::code::contract::ContractCode,
1920
precompiles,
2021
utils::{
2122
bump_data_fee,
@@ -24,6 +25,8 @@ use crate::{
2425
},
2526
};
2627

28+
use super::code::Code;
29+
2730
#[derive(Debug)]
2831
pub struct ActivationConfig {
2932
pub data_fee_bump_percent: u64,
@@ -71,7 +74,7 @@ pub async fn activate_contract(
7174
) -> Result<TransactionReceipt, ActivationError> {
7275
let code = provider.get_code_at(address).await?;
7376
let from_address = provider.default_signer_address();
74-
let data_fee = data_fee(code, address, config, &provider).await?;
77+
let data_fee = data_fee(&Code::new_from_code(&code), address, config, &provider).await?;
7578

7679
let receipt = precompiles::arb_wasm(&provider)
7780
.activateProgram(address)
@@ -93,20 +96,41 @@ pub async fn activate_contract(
9396

9497
/// Checks Stylus contract activation, returning the data fee.
9598
pub async fn data_fee(
96-
code: impl Into<Bytes>,
99+
code: &Code,
97100
address: Address,
98101
config: &ActivationConfig,
99102
provider: &impl Provider,
100103
) -> Result<U256, ActivationError> {
101104
let arbwasm = precompiles::arb_wasm(provider);
102105
let random_sender_addr = Address::random();
103106
let spoofed_sender_account = AccountOverride::default().with_balance(U256::MAX);
104-
let spoofed_code = AccountOverride::default().with_code(code);
105-
let state_override = StateOverride::from_iter([
106-
(address, spoofed_code),
107-
(random_sender_addr, spoofed_sender_account),
108-
]);
107+
let mut state_override = vec![(random_sender_addr, spoofed_sender_account)];
108+
109+
match code {
110+
Code::Contract(contract) => {
111+
let spoofed_code = AccountOverride::default().with_code(contract.as_slice().to_vec());
112+
state_override.push((address, spoofed_code));
113+
}
114+
Code::Fragments(fragments) => {
115+
let fragment_addresses = fragments.as_slice().iter().map(|fragment| {
116+
let fragment_address = Address::random();
117+
let fragment_code =
118+
AccountOverride::default().with_code(fragment.as_slice().to_vec());
119+
state_override.push((fragment_address, fragment_code));
120+
fragment_address
121+
});
122+
let root_contract = ContractCode::new_root_contract(
123+
fragments.uncompressed_wasm_size(),
124+
fragment_addresses,
125+
);
126+
127+
let spoofed_code =
128+
AccountOverride::default().with_code(root_contract.as_slice().to_vec());
129+
state_override.push((address, spoofed_code));
130+
}
131+
}
109132

133+
let state_override = StateOverride::from_iter(state_override);
110134
let result = arbwasm
111135
.activateProgram(address)
112136
.state(state_override)
@@ -135,7 +159,7 @@ pub async fn estimate_gas(
135159
) -> Result<u64, ActivationError> {
136160
let code = provider.get_code_at(address).await?;
137161
let from_address = provider.default_signer_address();
138-
let data_fee = data_fee(code, address, config, &provider).await?;
162+
let data_fee = data_fee(&Code::new_from_code(&code), address, config, &provider).await?;
139163

140164
let gas = precompiles::arb_wasm(&provider)
141165
.activateProgram(address)

stylus-tools/src/core/chain.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2025, Offchain Labs, Inc.
2+
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3+
4+
/// Maximum code size per EIP-170
5+
pub const DEFAULT_MAX_CODE_SIZE: u64 = 24_576;
6+
7+
#[derive(Debug)]
8+
pub struct ChainConfig {
9+
pub max_code_size: u64,
10+
}
11+
12+
impl Default for ChainConfig {
13+
fn default() -> Self {
14+
Self {
15+
max_code_size: DEFAULT_MAX_CODE_SIZE,
16+
}
17+
}
18+
}

stylus-tools/src/core/check.rs

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,25 @@ use crate::{
1010
core::{
1111
activation::{self, ActivationConfig},
1212
build::{build_contract, BuildConfig},
13+
code::{
14+
wasm::{compress_wasm, process_wasm_file},
15+
Code,
16+
},
1317
project::{
1418
contract::{Contract, ContractStatus},
1519
hash_project, ProjectConfig, ProjectHash,
1620
},
1721
},
1822
utils::format_file_size,
19-
wasm::process_wasm_file,
2023
};
2124

25+
use super::chain::ChainConfig;
26+
2227
#[derive(Debug, Default)]
2328
pub struct CheckConfig {
2429
pub activation: ActivationConfig,
2530
pub build: BuildConfig,
31+
pub chain: ChainConfig,
2632
pub project: ProjectConfig,
2733
}
2834

@@ -42,7 +48,7 @@ pub enum CheckError {
4248
#[error("{0}")]
4349
Project(#[from] crate::core::project::ProjectError),
4450
#[error("{0}")]
45-
ProcessWasm(#[from] crate::wasm::ProcessWasmFileError),
51+
Wasm(#[from] crate::core::code::wasm::WasmError),
4652
}
4753

4854
/// Checks that a contract is valid and can be deployed onchain.
@@ -70,29 +76,39 @@ pub async fn check_wasm_file(
7076
) -> Result<ContractStatus, CheckError> {
7177
debug!(@grey, "reading wasm file at {}", wasm_file.as_ref().to_string_lossy().lavender());
7278
let processed = process_wasm_file(wasm_file, project_hash)?;
73-
info!(@grey, "contract size: {}", format_file_size(ByteSize::b(processed.code.len() as u64), ByteSize::kib(16), ByteSize::kib(24)));
74-
debug!(@grey, "wasm size: {}", format_file_size(ByteSize::b(processed.wasm.len() as u64), ByteSize::kib(96), ByteSize::kib(128)));
79+
let compressed = compress_wasm(&processed)?;
80+
let code = Code::split_if_large(
81+
&compressed,
82+
processed.len() as u32,
83+
config.chain.max_code_size,
84+
);
85+
match &code {
86+
Code::Contract(c) => {
87+
info!(@grey, "contract size: {}", format_file_size(ByteSize::b(c.codesize() as u64), ByteSize::kib(16), ByteSize::kib(24)));
88+
}
89+
Code::Fragments(fs) => {
90+
info!(@grey, "contract size: {} ({} fragments)", format_file_size(ByteSize::b(fs.codesize() as u64), ByteSize::kib(16), ByteSize::kib(24)), fs.fragment_count());
91+
}
92+
}
93+
debug!(@grey, "wasm size: {}", format_file_size(ByteSize::b(processed.len() as u64), ByteSize::kib(96), ByteSize::kib(128)));
7594

7695
// Check if the contract already exists
7796
// TODO: check log
7897
debug!(@grey, "connecting to RPC: {:?}", provider.root());
79-
let codehash = processed.codehash();
80-
if Contract::exists(codehash, &provider).await? {
81-
return Ok(ContractStatus::Active {
82-
code: processed.code,
83-
});
98+
if let Some(codehash) = code.codehash() {
99+
if Contract::exists(codehash, &provider).await? {
100+
return Ok(ContractStatus::Active {
101+
code,
102+
wasm: processed,
103+
});
104+
}
84105
}
85106

86107
let contract_address = contract_address.unwrap_or_else(Address::random);
87-
let fee = activation::data_fee(
88-
processed.code.clone(),
89-
contract_address,
90-
&config.activation,
91-
provider,
92-
)
93-
.await?;
108+
let fee = activation::data_fee(&code, contract_address, &config.activation, provider).await?;
94109
Ok(ContractStatus::Ready {
95-
code: processed.code,
110+
code,
96111
fee,
112+
wasm: processed,
97113
})
98114
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2026, Offchain Labs, Inc.
2+
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
3+
4+
use alloy::primitives::{keccak256, Address, B256};
5+
6+
use crate::core::code::{prefixes, wasm::CompressedWasm};
7+
8+
/// Code for a contract which fits within MAX_CODE_SIZE
9+
#[derive(Debug)]
10+
pub struct ContractCode(pub Vec<u8>);
11+
12+
impl ContractCode {
13+
pub fn new(wasm: &CompressedWasm) -> Self {
14+
let mut code = Vec::with_capacity(prefixes::EOF_NO_DICT.len() + wasm.len());
15+
code.extend(prefixes::EOF_NO_DICT);
16+
code.extend(wasm.bytes());
17+
Self(code)
18+
}
19+
20+
pub fn new_root_contract(
21+
uncompressed_wasm_size: u32,
22+
addresses: impl IntoIterator<Item = Address>,
23+
) -> Self {
24+
let serialized_wasm_size = uncompressed_wasm_size.to_be_bytes();
25+
let addresses = addresses.into_iter();
26+
let mut code = Vec::with_capacity(
27+
prefixes::ROOT_NO_DICT.len()
28+
+ serialized_wasm_size.len()
29+
+ Address::len_bytes() * addresses.size_hint().1.unwrap_or(0),
30+
);
31+
code.extend(prefixes::ROOT_NO_DICT);
32+
code.extend(serialized_wasm_size);
33+
addresses.for_each(|a| code.extend(a));
34+
Self(code)
35+
}
36+
37+
pub fn new_from_code(code: &[u8]) -> Self {
38+
Self(code.to_vec())
39+
}
40+
41+
/// Get code bytes
42+
pub fn as_slice(&self) -> &[u8] {
43+
&self.0
44+
}
45+
46+
/// Codehash is keccak256 hash of the code bytes
47+
pub fn codehash(&self) -> B256 {
48+
keccak256(&self.0)
49+
}
50+
51+
/// Length of the contract code
52+
pub fn codesize(&self) -> usize {
53+
self.0.len()
54+
}
55+
}

0 commit comments

Comments
 (0)