Skip to content

Commit

Permalink
Polkadot: ink! 5.0 (#1632)
Browse files Browse the repository at this point in the history
Upgrade Solang to be compatible with [`ink!` v5.0
](https://use.ink/faq/migrating-from-ink-4-to-5).

- Simplify events
- Latest substrate node on CI
- Update tests and docs
  • Loading branch information
xermicus authored Apr 3, 2024
1 parent 5764e01 commit 28c86b3
Show file tree
Hide file tree
Showing 19 changed files with 535 additions and 307 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ jobs:
# We can't run substrate node as a github actions service, since it requires
# command line arguments. See https://github.com/actions/runner/pull/1152
- name: Start substrate
run: echo id=$(docker run -d -p 9944:9944 ghcr.io/hyperledger/solang-substrate-ci:ad6da01 substrate-contracts-node --dev --rpc-external -lwarn,runtime::contracts=trace) >> $GITHUB_OUTPUT
run: echo id=$(docker run -d -p 9944:9944 ghcr.io/hyperledger/solang-substrate-ci:62a8a6c substrate-contracts-node --dev --rpc-external -lwarn,runtime::contracts=trace) >> $GITHUB_OUTPUT
id: substrate
- uses: actions/download-artifact@v3
with:
Expand Down
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ anchor-syn = { version = "0.29.0", features = ["idl-build"] }
convert_case = "0.6"
parse-display = "0.9"
parity-scale-codec = "3.6"
ink_env = "4.3.0"
ink_metadata = "4.3.0"
ink_env = "5.0.0"
ink_metadata = "5.0.0"
scale-info = "2.10"
petgraph = "0.6"
wasmparser = "0.121.0"
Expand Down Expand Up @@ -89,7 +89,7 @@ borsh = "1.1"
borsh-derive = "1.1"
rayon = "1"
walkdir = "2.4"
ink_primitives = "4.3.0"
ink_primitives = "5.0.0"
wasm_host_attr = { path = "tests/wasm_host_attr" }
num-bigint = { version = "0.4", features = ["rand", "serde"]}

Expand Down
14 changes: 8 additions & 6 deletions docs/language/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ syntax.
be passed for Solana's ``sol_log_data`` system call, regardless if the ``indexed`` keyword is present or not.
This behavior follows what Solana's Anchor framework does.

In Polkadot, the topic fields are always the hash of the value of the field. Ethereum only hashes fields
which do not fit in the 32 bytes. Since a cryptographic hash is used, it is only possible to compare the topic against a
known value.
In Polkadot, field topics are culculated the same as in `ink! v5.0 <https://use.ink/basics/events/#topics>`_:
Topic fields are either the encoded value of the field or its blake2b256 hash
if the encoded value length exceeds 32 bytes.

An event can be declared in a contract, or outside.

Expand All @@ -49,8 +49,10 @@ make it clearer which exact event is being emitted.
.. include:: ../examples/event_positional_fields.sol
:code: solidity

In the transaction log, the first topic of an event is the keccak256 hash of the signature of the
event. The signature is the event name, followed by the fields types in a comma separated list in parentheses. So
the first topic for the second UserModified event would be the keccak256 hash of ``UserModified(address,uint64)``.
In the transaction log, the topic of an event is the blake2b256 hash of the signature of the
event. The signature is the event name, followed by the fields types in a comma separated list in parentheses,
like event signatures in `Ethereum Solidity <https://docs.soliditylang.org/en/v0.8.25/abi-spec.html#events>`_. So
the first topic for the second UserModified event would be the blake2b256 hash of ``UserModified(address,uint64)``.
You can leave this topic out by declaring the event ``anonymous``. This makes the event slightly smaller (32 bytes
less) and makes it possible to have 4 ``indexed`` fields rather than 3.

2 changes: 1 addition & 1 deletion integration/polkadot/UniswapV2Factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('UniswapV2Factory', () => {

let events: DecodedEvent[] = res0.contractEvents;
expect(events.length).toEqual(1)
expect(events[0].event.identifier).toBe('PairCreated')
expect(events[0].event.identifier).toBe('UniswapV2Factory::PairCreated')
expect(events[0].args[0].toString()).toBe(TEST_ADDRESSES[0])
expect(events[0].args[1].toString()).toBe(TEST_ADDRESSES[1])
expect(events[0].args[3].eq(1)).toBeTruthy();
Expand Down
69 changes: 22 additions & 47 deletions integration/polkadot/events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,62 +29,37 @@ describe('Deploy events contract and test event data, docs and topics', () => {

expect(events.length).toEqual(4);

expect(events[0].event.identifier).toBe("foo1");
expect(events[0].event.identifier).toBe("Events::foo1");
expect(events[0].event.docs).toEqual(["Ladida tada"]);
expect(events[0].args.map(a => a.toJSON())).toEqual([254, "hello there"]);

expect(events[1].event.identifier).toBe("foo2");
expect(events[1].event.identifier).toBe("Events::foo2");
expect(events[1].event.docs).toEqual(["Event Foo2\n\nJust a test\n\nAuthor: them is me"]);
expect(events[1].args.map(a => a.toJSON())).toEqual(["0x7fffffffffffffff", "minor", deploy_contract.address.toString()]);

expect(events[2].event.identifier).toBe("ThisEventTopicShouldGetHashed");
expect(events[2].event.identifier).toBe("Events::ThisEventTopicShouldGetHashed");
expect(events[2].args.map(a => a.toJSON())).toEqual([alice.address]);

// In ink! the 3rd event does look like this:
//
// #[ink(event)]
// pub struct ThisEventTopicShouldGetHashed {
// #[ink(topic)]
// caller: AccountId,
// }
//
// It yields the following event topics:
//
// topics: [
// 0x5dde952854d38c37cff349bfc574a48a831de385b82457a5c25d9d39c220f3a7
// 0xa5af79de4a26a64813f980ffbb64ac0d7c278f67b17721423daed26ec5d3fe51
// ]
//
// So we expect our solidity contract to produce the exact same topics:

let hashed_event_topics = await conn.query.system.eventTopics("0x5dde952854d38c37cff349bfc574a48a831de385b82457a5c25d9d39c220f3a7");
expect(hashed_event_topics.length).toBe(1);
let hashed_topics = await conn.query.system.eventTopics("0xa5af79de4a26a64813f980ffbb64ac0d7c278f67b17721423daed26ec5d3fe51");
expect(hashed_topics.length).toBe(1);

expect(events[3].event.identifier).toBe("Event");
// Expect the 3rd event to yield the following event topics:
// - blake2x256 sum of its signature: 'ThisEventTopicShouldGetHashed(address)'
// - Address of the caller

let field_topic = await conn.query.system.eventTopics(alice.addressRaw);
expect(field_topic.length).toBe(1);

let event_topic = await conn.query.system.eventTopics("0x95c29b3e1b835071ab157a22d89cfc81d176add91127a1ee8766abf406a2cbc3");
expect(event_topic.length).toBe(1);

expect(events[3].event.identifier).toBe("Events::Event");
expect(events[3].args.map(a => a.toJSON())).toEqual([true]);

// In ink! the 4th event does look like this:
//
// #[ink(event)]
// pub struct Event {
// #[ink(topic)]
// something: bool,
// }
//
// It yields the following event topics:
//
// topics: [
// 0x004576656e74733a3a4576656e74000000000000000000000000000000000000
// 0x604576656e74733a3a4576656e743a3a736f6d657468696e6701000000000000
// ]
//
// So we expect our solidity contract to produce the exact same topics:

let unhashed_event_topics = await conn.query.system.eventTopics("0x004576656e74733a3a4576656e74000000000000000000000000000000000000");
expect(unhashed_event_topics.length).toBe(1);
let unhashed_topics = await conn.query.system.eventTopics("0x604576656e74733a3a4576656e743a3a736f6d657468696e6701000000000000");
expect(unhashed_topics.length).toBe(1);
// The 4th event yields the following event topics:
// - blake2x256 sum of its signature: 'Event(bool)'
// - unhashed data (because encoded length is <= 32 bytes) of 'true'

field_topic = await conn.query.system.eventTopics("0x0100000000000000000000000000000000000000000000000000000000000000");
expect(field_topic.length).toBe(1);
event_topic = await conn.query.system.eventTopics("0xc2bc7a077121efada8bc6a0af16c1e886406e8c2d1716979cb1b92098d8b49bc");
expect(event_topic.length).toBe(1);
});
});
2 changes: 1 addition & 1 deletion integration/polkadot/msg_sender.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('Deploy mytoken contract and test', () => {

expect(events.length).toEqual(1);

expect(events[0].event.identifier).toBe("Debugging");
expect(events[0].event.identifier).toBe("mytokenEvent::Debugging");
expect(events[0].args.map(a => a.toJSON())).toEqual([alice.address]);
});
});
3 changes: 2 additions & 1 deletion integration/polkadot/upgradeable_proxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ describe('Deploy the upgradable proxy and implementations; expect the upgrade me
let result: any = await transaction(proxy.tx.upgradeToAndCall({ gasLimit }, ...params), aliceKeypair());

let events: DecodedEvent[] = result.contractEvents;
console.log(events);
expect(events.length).toEqual(1);
expect(events[0].event.identifier).toBe("Upgraded");
expect(events[0].event.identifier).toBe("UpgradeableProxy::Upgraded");
expect(events[0].args.map(a => a.toJSON())[0]).toEqual(params[0].toJSON());
}

Expand Down
4 changes: 2 additions & 2 deletions integration/subxt-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ serde_json = "1.0.96"
sp-keyring = "24.0.0"
subxt = { version = "0.31.0", features = ["substrate-compat"] }
tokio = {version = "1.28.2", features = ["rt-multi-thread", "macros", "time"]}
contract-metadata = "3.0.1"
contract-transcode = "3.0.1"
contract-metadata = "4.0.2"
contract-transcode = "4.0.2"

[workspace]
members = []
Expand Down
2 changes: 1 addition & 1 deletion integration/subxt-tests/src/cases/asserts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ async fn case() -> anyhow::Result<()> {
.unwrap_err();
assert!(res
.to_string()
.contains("ModuleError { index: 8, error: [25, 0, 0, 0] }"));
.contains("ModuleError { index: 8, error: [26, 0, 0, 0] }"));

// state should not change after failed operation
let rv = contract
Expand Down
6 changes: 0 additions & 6 deletions integration/subxt-tests/src/cases/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ async fn case() -> anyhow::Result<()> {

let e1_buffer = &mut e1.data.as_slice();

let topic = e1_buffer.read_byte()?;
assert_eq!(topic, 0);

// mimic the solidity struct type
#[derive(Decode)]
struct Foo1 {
Expand All @@ -48,9 +45,6 @@ async fn case() -> anyhow::Result<()> {
let e2 = &rs[1];
let e2_buffer = &mut e2.data.as_slice();

let topic = e2_buffer.read_byte()?;
assert_eq!(topic, 1);

// mimic the solidity struct type
#[derive(Decode)]
struct Foo2 {
Expand Down
4 changes: 0 additions & 4 deletions integration/subxt-tests/src/cases/msg_sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,6 @@ async fn case() -> anyhow::Result<()> {

let mut evt_buffer = evt.data.as_slice();

let topic_id = evt_buffer.read_byte()?;

assert_eq!(topic_id, 0);

let addr = <AccountId32>::decode(&mut evt_buffer)?;

assert_eq!(addr, sp_keyring::AccountKeyring::Alice.to_account_id());
Expand Down
35 changes: 25 additions & 10 deletions src/abi/polkadot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use contract_metadata::{
CodeHash, Compiler, Contract, ContractMetadata, Language, Source, SourceCompiler,
SourceLanguage, SourceWasm,
};
use ink_env::hash::{Blake2x256, CryptoHash};
use ink_metadata::{
layout::{FieldLayout, Layout, LayoutKey, LeafLayout, RootLayout, StructLayout},
ConstructorSpec, ContractSpec, EnvironmentSpec, EventParamSpec, EventSpec, InkProject,
Expand All @@ -21,6 +22,7 @@ use semver::Version;
use solang_parser::pt;

use crate::{
codegen::polkadot::SCRATCH_SIZE,
codegen::revert::{SolidityError, ERROR_SELECTOR, PANIC_SELECTOR},
sema::{
ast::{self, ArrayLength, EventDecl, Function},
Expand Down Expand Up @@ -285,7 +287,7 @@ fn type_to_storage_layout(
}

/// Generate `InkProject` from `ast::Type` and `ast::Namespace`
pub fn gen_project(contract_no: usize, ns: &ast::Namespace) -> InkProject {
pub fn gen_project<'a>(contract_no: usize, ns: &'a ast::Namespace) -> InkProject {
let mut registry = PortableRegistryBuilder::new();

// This is only used by off-chain tooling. At the moment there is no such tooling available yet.
Expand All @@ -302,6 +304,7 @@ pub fn gen_project(contract_no: usize, ns: &ast::Namespace) -> InkProject {
let root = RootLayout::new(
layout_key,
type_to_storage_layout(ty, layout_key, &registry),
ty.into(),
);
Some(FieldLayout::new(var.name.clone(), root))
} else {
Expand Down Expand Up @@ -341,7 +344,7 @@ pub fn gen_project(contract_no: usize, ns: &ast::Namespace) -> InkProject {
.payable(payable)
.args(args)
.docs(vec![render(&f.tags).as_str()])
.returns(ReturnTypeSpec::new(None))
.returns(ReturnTypeSpec::new(TypeSpec::default()))
.done()
};

Expand Down Expand Up @@ -407,7 +410,7 @@ pub fn gen_project(contract_no: usize, ns: &ast::Namespace) -> InkProject {
Some(TypeSpec::new(ty.into(), path))
}
};
let ret_type = ReturnTypeSpec::new(ret_spec);
let ret_type = ReturnTypeSpec::new(ret_spec.unwrap_or_default());
let args = f
.params
.iter()
Expand Down Expand Up @@ -445,7 +448,8 @@ pub fn gen_project(contract_no: usize, ns: &ast::Namespace) -> InkProject {
.map(message_spec)
.collect::<Vec<MessageSpec<PortableForm>>>();

let mut event_spec = |e: &EventDecl| -> EventSpec<PortableForm> {
// ink! v5 ABI wants declared events to be unique; collect the signature into a HashMap
let mut event_spec = |e: &'a EventDecl| -> (&'a str, EventSpec<PortableForm>) {
let args = e
.fields
.iter()
Expand All @@ -460,19 +464,29 @@ pub fn gen_project(contract_no: usize, ns: &ast::Namespace) -> InkProject {
.done()
})
.collect::<Vec<_>>();
EventSpec::new(e.id.name.clone())
let topic = (!e.anonymous).then(|| {
let mut buf = [0; 32];
<Blake2x256 as CryptoHash>::hash(e.signature.as_bytes(), &mut buf);
buf
});
let event = EventSpec::new(e.id.name.clone())
.args(args)
.docs(vec![render(&e.tags).as_str()])
.done()
.signature_topic(topic)
.module_path(ns.contracts[contract_no].id.name.as_str())
.done();
let signature = e.signature.as_str();

(signature, event)
};

let events = ns.contracts[contract_no]
.emits_events
.iter()
.map(|event_no| {
let event = &ns.events[*event_no];
event_spec(event)
})
.map(|event_no| event_spec(&ns.events[*event_no]))
.collect::<std::collections::HashMap<&str, EventSpec<PortableForm>>>()
.drain()
.map(|(_, spec)| spec)
.collect::<Vec<EventSpec<PortableForm>>>();

let environment: EnvironmentSpec<PortableForm> = EnvironmentSpec::new()
Expand Down Expand Up @@ -510,6 +524,7 @@ pub fn gen_project(contract_no: usize, ns: &ast::Namespace) -> InkProject {
primitive_to_ty(&ast::Type::Uint(64), &mut registry).into(),
path!("Timestamp"),
))
.static_buffer_size(SCRATCH_SIZE as usize)
.done();

let mut error_definitions = vec![
Expand Down
Loading

0 comments on commit 28c86b3

Please sign in to comment.