diff --git a/src/codegen/encoding/soroban_encoding.rs b/src/codegen/encoding/soroban_encoding.rs index e3a210692..9469e2198 100644 --- a/src/codegen/encoding/soroban_encoding.rs +++ b/src/codegen/encoding/soroban_encoding.rs @@ -142,7 +142,7 @@ pub fn soroban_decode_arg( }, Type::Uint(64) => decode_u64(wrapper_cfg, vartab, arg), - Type::Address(_) | Type::String => arg.clone(), + Type::Address(_) | Type::String | Type::DynamicBytes => arg.clone(), Type::Enum(enum_no) => { let decoded = soroban_decode_arg(arg, wrapper_cfg, vartab, ns, Some(Type::Uint(32))); @@ -209,6 +209,75 @@ pub fn soroban_decode_arg( } } + Type::Bytes(n) => { + let n_expr = Expression::NumberLiteral { + loc: Loc::Codegen, + ty: Type::Uint(32), + value: BigInt::from(n as u64), + }; + + let buf_var = vartab.temp_name("bytes_buf", &Type::DynamicBytes); + wrapper_cfg.add( + vartab, + Instr::Set { + loc: Loc::Codegen, + res: buf_var, + expr: Expression::AllocDynamicBytes { + loc: Loc::Codegen, + ty: Type::DynamicBytes, + size: Box::new(n_expr.clone()), + initializer: None, + }, + }, + ); + let buf = Expression::Variable { + loc: Loc::Codegen, + ty: Type::DynamicBytes, + var_no: buf_var, + }; + + let dest_ptr = Expression::VectorData { + pointer: Box::new(buf.clone()), + }; + let dest_encoded = zext_shift_add(Loc::Codegen, dest_ptr, 32, 4); + let src_off_encoded = zext_shift_add( + Loc::Codegen, + Expression::NumberLiteral { + loc: Loc::Codegen, + ty: Type::Uint(32), + value: BigInt::from(0u64), + }, + 32, + 4, + ); + let len_encoded = zext_shift_add(Loc::Codegen, n_expr, 32, 4); + + let unused = vartab.temp_name("bytes_copy_ret", &Type::Uint(64)); + wrapper_cfg.add( + vartab, + Instr::Call { + res: vec![unused], + return_tys: vec![Type::Uint(64)], + call: InternalCallTy::HostFunction { + name: HostFunctions::BytesCopyToLinearMemory.name().to_string(), + }, + args: vec![arg, src_off_encoded, dest_encoded, len_encoded], + }, + ); + + Expression::Load { + loc: Loc::Codegen, + ty: Type::Bytes(n), + expr: Box::new(Expression::Cast { + loc: Loc::Codegen, + ty: Type::Ref(Box::new(Type::DynamicBytes)), + expr: Box::new(Expression::VectorData { + pointer: Box::new(buf), + }), + }), + } + } + _ => unimplemented!("unimplemented ty {:#?} in soroban decoder", ty), } } @@ -721,6 +790,74 @@ pub fn soroban_encode_arg( res: obj, expr: encode_vector(item.clone(), cfg, vartab), }, + Type::DynamicBytes => Instr::Set { + loc: Loc::Codegen, + res: obj, + expr: item.clone(), + }, + + Type::Bytes(n) => { + let n_val = BigInt::from(n as u64); + + let n_expr = Expression::NumberLiteral { + loc: item.loc(), + ty: Type::Uint(32), + value: n_val.clone(), + }; + + let buf_var = vartab.temp_name("bytes_spill", &Type::DynamicBytes); + cfg.add( + vartab, + Instr::Set { + loc: item.loc(), + res: buf_var, + expr: Expression::AllocDynamicBytes { + loc: item.loc(), + ty: Type::DynamicBytes, + size: Box::new(n_expr.clone()), + initializer: None, + }, + }, + ); + let buf = Expression::Variable { + loc: item.loc(), + ty: Type::DynamicBytes, + var_no: buf_var, + }; + + cfg.add( + vartab, + Instr::Store { + dest: Expression::Cast { + loc: item.loc(), + ty: Type::Ref(Box::new(Type::DynamicBytes)), + expr: Box::new(Expression::VectorData { + pointer: Box::new(buf.clone()), + }), + }, + data: item.clone(), + }, + ); + + let encoded_ptr = zext_shift_add( + item.loc(), + Expression::VectorData { + pointer: Box::new(buf), + }, + 32, + 4, + ); + let encoded_len = zext_shift_add(item.loc(), n_expr, 32, 4); + + Instr::Call { + res: vec![obj], + return_tys: vec![Type::Uint(64)], + call: InternalCallTy::HostFunction { + name: HostFunctions::BytesNewFromLinearMemory.name().to_string(), + }, + args: vec![encoded_ptr, encoded_len], + } + } _ => todo!("Type not yet supported in soroban encoder: {:?}", item.ty()), }; diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index f87fd8ce9..441a55950 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -144,6 +144,8 @@ pub enum HostFunctions { BytesNewFromLinearMemory, BytesLen, BytesCopyToLinearMemory, + BytesGet, + BytesPut, } impl HostFunctions { @@ -192,6 +194,8 @@ impl HostFunctions { HostFunctions::BytesNewFromLinearMemory => "b.3", HostFunctions::BytesLen => "b.8", HostFunctions::BytesCopyToLinearMemory => "b.1", + HostFunctions::BytesGet => "b.6", + HostFunctions::BytesPut => "b.5", HostFunctions::VecLen => "v.3", HostFunctions::VecPopBack => "v.7", HostFunctions::VecGet => "v.1", diff --git a/src/emit/binary.rs b/src/emit/binary.rs index fa28f823d..84b59cd95 100644 --- a/src/emit/binary.rs +++ b/src/emit/binary.rs @@ -888,11 +888,8 @@ impl<'a> Binary<'a> { fn var_ty_uses_pointer_storage(&self, ty: &Type) -> bool { match ty.deref_memory() { - Type::Struct(_) - | Type::Array(..) - | Type::DynamicBytes - | Type::ExternalFunction { .. } => true, - Type::String => self.ns.target != Target::Soroban, + Type::Struct(_) | Type::Array(..) | Type::ExternalFunction { .. } => true, + Type::String | Type::DynamicBytes => self.ns.target != Target::Soroban, _ => false, } } diff --git a/src/emit/soroban/mod.rs b/src/emit/soroban/mod.rs index 542a2626c..308720cb0 100644 --- a/src/emit/soroban/mod.rs +++ b/src/emit/soroban/mod.rs @@ -156,6 +156,14 @@ impl HostFunctions { .context .i64_type() .fn_type(&[ty.into(), ty.into(), ty.into(), ty.into()], false), + HostFunctions::BytesGet => bin + .context + .i64_type() + .fn_type(&[ty.into(), ty.into()], false), + HostFunctions::BytesPut => bin + .context + .i64_type() + .fn_type(&[ty.into(), ty.into(), ty.into()], false), } } } @@ -338,7 +346,9 @@ impl SorobanTarget { ast::Type::Uint(256) => ScSpecTypeDef::U256, ast::Type::Bool => ScSpecTypeDef::Bool, ast::Type::Address(_) => ScSpecTypeDef::Address, - ast::Type::Bytes(_) => ScSpecTypeDef::Bytes, + ast::Type::Bytes(_) | ast::Type::DynamicBytes => { + ScSpecTypeDef::Bytes + } ast::Type::String => ScSpecTypeDef::String, ast::Type::Array(ty, _) => { let element = Self::vec_spec_type(ty.as_ref()); @@ -377,7 +387,7 @@ impl SorobanTarget { ast::Type::Int(_) => ScSpecTypeDef::I32, ast::Type::Bool => ScSpecTypeDef::Bool, ast::Type::Address(_) => ScSpecTypeDef::Address, - ast::Type::Bytes(_) => ScSpecTypeDef::Bytes, + ast::Type::Bytes(_) | ast::Type::DynamicBytes => ScSpecTypeDef::Bytes, ast::Type::String => ScSpecTypeDef::String, ast::Type::Void => ScSpecTypeDef::Void, ast::Type::Struct(_) => ScSpecTypeDef::Void, // TODO: Map struct types. @@ -466,6 +476,9 @@ impl SorobanTarget { HostFunctions::GetCurrentContractAddress, HostFunctions::BytesNewFromLinearMemory, HostFunctions::BytesCopyToLinearMemory, + HostFunctions::BytesLen, + HostFunctions::BytesGet, + HostFunctions::BytesPut, HostFunctions::VecLen, HostFunctions::VecPopBack, ]; diff --git a/src/emit/soroban/target.rs b/src/emit/soroban/target.rs index eb30f29f9..c9d3dd858 100644 --- a/src/emit/soroban/target.rs +++ b/src/emit/soroban/target.rs @@ -290,7 +290,115 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { index: IntValue<'a>, loc: Loc, ) -> IntValue<'a> { - unimplemented!() + let bytes_obj = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::GetContractData.name()) + .unwrap(), + &[ + slot.into(), + bin.context.i64_type().const_int(1, false).into(), + ], + "load_bytes_storage", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let len_u32val = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::BytesLen.name()) + .unwrap(), + &[bytes_obj.into()], + "bytes_len", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let len_decoded = bin + .builder + .build_right_shift( + len_u32val, + bin.context.i64_type().const_int(32, false), + false, + "len_decoded", + ) + .unwrap(); + + let index64 = if index.get_type().get_bit_width() == 64 { + index + } else { + bin.builder + .build_int_z_extend(index, bin.context.i64_type(), "index64") + .unwrap() + }; + + let in_range = bin + .builder + .build_int_compare(inkwell::IntPredicate::ULT, index64, len_decoded, "in_range") + .unwrap(); + + let get_block = bin + .context + .append_basic_block(function, "bytes_get_in_range"); + let oob_block = bin.context.append_basic_block(function, "bytes_get_oob"); + + bin.builder + .build_conditional_branch(in_range, get_block, oob_block) + .unwrap(); + + bin.builder.position_at_end(oob_block); + bin.log_runtime_error( + self, + "storage bytes index out of bounds".to_string(), + Some(loc), + ); + self.assert_failure( + bin, + bin.context.ptr_type(Default::default()).const_null(), + bin.context.i32_type().const_zero(), + ); + + bin.builder.position_at_end(get_block); + + let index_encoded = encode_value(index64, 32, 4, bin); + + let result_u32val = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::BytesGet.name()) + .unwrap(), + &[bytes_obj.into(), index_encoded.into()], + "byte_val_u32val", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let raw_u64 = bin + .builder + .build_right_shift( + result_u32val, + bin.context.i64_type().const_int(32, false), + false, + "raw_u64", + ) + .unwrap(); + + bin.builder + .build_int_truncate(raw_u64, bin.context.i8_type(), "byte_val") + .unwrap() } fn set_storage_bytes_subscript( @@ -302,7 +410,124 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { value: IntValue<'a>, loc: Loc, ) { - unimplemented!() + let bytes_obj = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::GetContractData.name()) + .unwrap(), + &[ + slot.into(), + bin.context.i64_type().const_int(1, false).into(), + ], + "load_bytes_storage", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let len_u32val = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::BytesLen.name()) + .unwrap(), + &[bytes_obj.into()], + "bytes_len", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + let len_decoded = bin + .builder + .build_right_shift( + len_u32val, + bin.context.i64_type().const_int(32, false), + false, + "len_decoded", + ) + .unwrap(); + + let index64 = if index.get_type().get_bit_width() == 64 { + index + } else { + bin.builder + .build_int_z_extend(index, bin.context.i64_type(), "index64") + .unwrap() + }; + + let in_range = bin + .builder + .build_int_compare(inkwell::IntPredicate::ULT, index64, len_decoded, "in_range") + .unwrap(); + + let set_block = bin + .context + .append_basic_block(function, "bytes_set_in_range"); + let oob_block = bin.context.append_basic_block(function, "bytes_set_oob"); + + bin.builder + .build_conditional_branch(in_range, set_block, oob_block) + .unwrap(); + + bin.builder.position_at_end(oob_block); + bin.log_runtime_error( + self, + "storage bytes index out of bounds".to_string(), + Some(loc), + ); + self.assert_failure( + bin, + bin.context.ptr_type(Default::default()).const_null(), + bin.context.i32_type().const_zero(), + ); + + bin.builder.position_at_end(set_block); + + let index_encoded = encode_value(index64, 32, 4, bin); + + let value64 = if value.get_type().get_bit_width() == 64 { + value + } else { + bin.builder + .build_int_z_extend(value, bin.context.i64_type(), "value64") + .unwrap() + }; + let value_encoded = encode_value(value64, 32, 4, bin); + + let new_bytes_obj = bin + .builder + .build_call( + bin.module + .get_function(HostFunctions::BytesPut.name()) + .unwrap(), + &[bytes_obj.into(), index_encoded.into(), value_encoded.into()], + "new_bytes_obj", + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + bin.builder + .build_call( + bin.module + .get_function(HostFunctions::PutContractData.name()) + .unwrap(), + &[ + slot.into(), + new_bytes_obj.into(), + bin.context.i64_type().const_int(1, false).into(), + ], + "store_bytes_storage", + ) + .unwrap(); } fn storage_subscript( diff --git a/tests/soroban_testcases/bytes_n_codec.rs b/tests/soroban_testcases/bytes_n_codec.rs new file mode 100644 index 000000000..e1a40eba7 --- /dev/null +++ b/tests/soroban_testcases/bytes_n_codec.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::build_solidity; +use soroban_sdk::{BytesN, FromVal, IntoVal}; + +#[test] +fn echo_bytes1() { + let src = build_solidity( + r#"contract BytesNCodec { + function echo_bytes1(bytes1 x) public pure returns (bytes1) { + return x; + } + }"#, + |_| {}, + ); + let addr = src.contracts.last().unwrap(); + + let payload = BytesN::<1>::from_array(&src.env, &[0xAB]); + let res = src.invoke_contract( + addr, + "echo_bytes1", + vec![payload.clone().into_val(&src.env)], + ); + assert_eq!(BytesN::<1>::from_val(&src.env, &res), payload); +} + +#[test] +fn echo_bytes5() { + let src = build_solidity( + r#"contract BytesNCodec { + function echo_bytes5(bytes5 x) public pure returns (bytes5) { + return x; + } + }"#, + |_| {}, + ); + let addr = src.contracts.last().unwrap(); + + let payload = BytesN::<5>::from_array(&src.env, &[0x01, 0x02, 0x03, 0x04, 0x05]); + let res = src.invoke_contract( + addr, + "echo_bytes5", + vec![payload.clone().into_val(&src.env)], + ); + assert_eq!(BytesN::<5>::from_val(&src.env, &res), payload); +} + +#[test] +fn echo_bytes32() { + let src = build_solidity( + r#"contract BytesNCodec { + function echo_bytes32(bytes32 x) public pure returns (bytes32) { + return x; + } + }"#, + |_| {}, + ); + let addr = src.contracts.last().unwrap(); + + let payload = BytesN::<32>::from_array( + &src.env, + &[ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, + 0xEE, 0xFF, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, + 0x33, 0x22, 0x11, 0x00, + ], + ); + let res = src.invoke_contract( + addr, + "echo_bytes32", + vec![payload.clone().into_val(&src.env)], + ); + assert_eq!(BytesN::<32>::from_val(&src.env, &res), payload); +} diff --git a/tests/soroban_testcases/bytes_subscript.rs b/tests/soroban_testcases/bytes_subscript.rs new file mode 100644 index 000000000..5d9ecda6f --- /dev/null +++ b/tests/soroban_testcases/bytes_subscript.rs @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::{build_solidity, SorobanEnv}; +use soroban_sdk::{Address, Bytes, BytesN, FromVal, IntoVal}; + +const SOURCE: &str = r#"contract BytesSubscript { + bytes public data; + + function set_data(bytes memory d) public { + data = d; + } + + function get_byte(uint32 i) public view returns (bytes1) { + return data[i]; + } + + function set_byte(uint32 i, bytes1 v) public { + data[i] = v; + } +}"#; + +/// deploys the contract and sets `data` with `payload`. returns env and contract address. +fn setup(payload: &[u8]) -> (SorobanEnv, Address) { + let src = build_solidity(SOURCE, |_| {}); + let addr = src.contracts.last().unwrap().clone(); + let payload_bytes = Bytes::from_slice(&src.env, payload); + src.invoke_contract(&addr, "set_data", vec![payload_bytes.into_val(&src.env)]); + (src, addr) +} + +// solidity function get_byte wrapper +fn get_byte(src: &SorobanEnv, addr: &Address, i: u32) -> u8 { + let raw = src.invoke_contract(addr, "get_byte", vec![i.into_val(&src.env)]); + let byte = BytesN::<1>::from_val(&src.env, &raw); + byte.to_array()[0] +} + +// solidity function set_byte wrapper +fn set_byte(src: &SorobanEnv, addr: &Address, i: u32, v: u8) { + let byte = BytesN::<1>::from_array(&src.env, &[v]); + src.invoke_contract( + addr, + "set_byte", + vec![i.into_val(&src.env), byte.into_val(&src.env)], + ); +} + +#[test] +fn get_byte_reads_each_position() { + let (src, addr) = setup(b"hello"); + + assert_eq!(get_byte(&src, &addr, 0), b'h'); + assert_eq!(get_byte(&src, &addr, 1), b'e'); + assert_eq!(get_byte(&src, &addr, 2), b'l'); + assert_eq!(get_byte(&src, &addr, 3), b'l'); + assert_eq!(get_byte(&src, &addr, 4), b'o'); +} + +#[test] +fn set_byte_at_first_index() { + let (src, addr) = setup(b"hello"); + + set_byte(&src, &addr, 0, b'J'); + + assert_eq!(get_byte(&src, &addr, 0), b'J'); +} + +#[test] +fn set_byte_at_last_index() { + let (src, addr) = setup(b"hello"); + + set_byte(&src, &addr, 4, b'!'); + + assert_eq!(get_byte(&src, &addr, 4), b'!'); +} + +#[test] +fn set_byte_preserves_other_bytes() { + let (src, addr) = setup(b"hello"); + + set_byte(&src, &addr, 2, b'X'); + + assert_eq!(get_byte(&src, &addr, 0), b'h'); + assert_eq!(get_byte(&src, &addr, 1), b'e'); + assert_eq!(get_byte(&src, &addr, 2), b'X'); + assert_eq!(get_byte(&src, &addr, 3), b'l'); + assert_eq!(get_byte(&src, &addr, 4), b'o'); +} + +#[test] +fn set_byte_repeated_keeps_latest() { + let (src, addr) = setup(b"hello"); + + set_byte(&src, &addr, 1, b'X'); + set_byte(&src, &addr, 1, b'Y'); + set_byte(&src, &addr, 1, b'Z'); + + assert_eq!(get_byte(&src, &addr, 1), b'Z'); +} + +#[test] +fn get_byte_out_of_bounds_traps() { + let (src, addr) = setup(b"hello"); + + let logs = src.invoke_contract_expect_error(&addr, "get_byte", vec![5u32.into_val(&src.env)]); + assert!( + logs.iter() + .any(|l| l.contains("storage bytes index out of bounds")), + "expected OOB log, got: {logs:?}" + ); +} + +#[test] +fn set_byte_out_of_bounds_traps() { + let (src, addr) = setup(b"hello"); + + let byte = BytesN::<1>::from_array(&src.env, &[b'X']); + let logs = src.invoke_contract_expect_error( + &addr, + "set_byte", + vec![5u32.into_val(&src.env), byte.into_val(&src.env)], + ); + assert!( + logs.iter() + .any(|l| l.contains("storage bytes index out of bounds")), + "expected OOB log, got: {logs:?}" + ); +} + +#[test] +fn get_byte_on_empty_bytes_traps() { + let (src, addr) = setup(b""); + + let logs = src.invoke_contract_expect_error(&addr, "get_byte", vec![0u32.into_val(&src.env)]); + assert!( + logs.iter() + .any(|l| l.contains("storage bytes index out of bounds")), + "expected OOB log, got: {logs:?}" + ); +} diff --git a/tests/soroban_testcases/dynamic_bytes.rs b/tests/soroban_testcases/dynamic_bytes.rs new file mode 100644 index 000000000..070eeae75 --- /dev/null +++ b/tests/soroban_testcases/dynamic_bytes.rs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::build_solidity; +use soroban_sdk::{Bytes, FromVal, IntoVal}; + +#[test] +fn set_and_get_bytes() { + let src = build_solidity( + r#"contract BytesWriteStub { + bytes public data; + + function set_data(bytes memory d) public { + data = d; + } + }"#, + |_| {}, + ); + + let addr = src.contracts.last().unwrap(); + + // empty bytes round-trip + let empty = Bytes::from_slice(&src.env, b""); + src.invoke_contract(addr, "set_data", vec![empty.clone().into_val(&src.env)]); + let res = src.invoke_contract(addr, "data", vec![]); + assert_eq!(Bytes::from_val(&src.env, &res), empty); + + // non-empty bytes round-trip + let payload = Bytes::from_slice(&src.env, b"hello"); + src.invoke_contract(addr, "set_data", vec![payload.clone().into_val(&src.env)]); + let res = src.invoke_contract(addr, "data", vec![]); + assert_eq!(Bytes::from_val(&src.env, &res), payload); +} diff --git a/tests/soroban_testcases/mod.rs b/tests/soroban_testcases/mod.rs index cdd404e6f..4c702f2e9 100644 --- a/tests/soroban_testcases/mod.rs +++ b/tests/soroban_testcases/mod.rs @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 mod alloc; mod array_args; +mod bytes_n_codec; +mod bytes_subscript; +mod dynamic_bytes; mod atomic_swap; mod auth; mod constructor;