From 20a9eb54f570ced435c0decf2e985c8560a0f926 Mon Sep 17 00:00:00 2001 From: julio4 Date: Wed, 14 Feb 2024 19:25:23 +0900 Subject: [PATCH 1/6] SmartContract: removed `sender` getter, added `sender.getUnconstrained` and `sender.getAndRequireSignature` --- .../mina/account-update-layout.unit-test.ts | 6 +- src/lib/zkapp.ts | 61 +++++++++++-------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/lib/mina/account-update-layout.unit-test.ts b/src/lib/mina/account-update-layout.unit-test.ts index 4adaeca841..0be36c05f4 100644 --- a/src/lib/mina/account-update-layout.unit-test.ts +++ b/src/lib/mina/account-update-layout.unit-test.ts @@ -7,12 +7,14 @@ import { SmartContract, method } from '../zkapp.js'; class NestedCall extends SmartContract { @method deposit() { - let payerUpdate = AccountUpdate.createSigned(this.sender); + let sender = this.sender.getUnconstrained(); + let payerUpdate = AccountUpdate.createSigned(sender); payerUpdate.send({ to: this.address, amount: UInt64.one }); } @method depositUsingTree() { - let payerUpdate = AccountUpdate.createSigned(this.sender); + let sender = this.sender.getUnconstrained(); + let payerUpdate = AccountUpdate.createSigned(sender); let receiverUpdate = AccountUpdate.create(this.address); payerUpdate.send({ to: receiverUpdate, amount: UInt64.one }); diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index d927c490ae..bea4e9a9ea 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -799,31 +799,44 @@ super.init(); #_senderState: { sender: PublicKey; transactionId: number }; - /** - * The public key of the current transaction's sender account. - * - * Throws an error if not inside a transaction, or the sender wasn't passed in. - * - * **Warning**: The fact that this public key equals the current sender is not part of the proof. - * A malicious prover could use any other public key without affecting the validity of the proof. - */ - get sender(): PublicKey { - // TODO this logic now has some overlap with this.self, we should combine them somehow - // (but with care since the logic in this.self is a bit more complicated) - if (!Mina.currentTransaction.has()) { - throw Error( - `this.sender is not available outside a transaction. Make sure you only use it within \`Mina.transaction\` blocks or smart contract methods.` - ); - } - let transactionId = Mina.currentTransaction.id(); - if (this.#_senderState?.transactionId === transactionId) { - return this.#_senderState.sender; - } else { - let sender = Provable.witness(PublicKey, () => Mina.sender()); - this.#_senderState = { transactionId, sender }; + sender: { + self: SmartContract; + getUnconstrained(): PublicKey; + getAndRequireSignature(): PublicKey; + } = { + self: this, + /** + * The public key of the current transaction's sender account. + * + * Throws an error if not inside a transaction, or the sender wasn't passed in. + * + * **Warning**: The fact that this public key equals the current sender is not part of the proof. + * A malicious prover could use any other public key without affecting the validity of the proof. + */ + getUnconstrained(): PublicKey { + // TODO this logic now has some overlap with this.self, we should combine them somehow + // (but with care since the logic in this.self is a bit more complicated) + if (!Mina.currentTransaction.has()) { + throw Error( + `this.sender is not available outside a transaction. Make sure you only use it within \`Mina.transaction\` blocks or smart contract methods.` + ); + } + let transactionId = Mina.currentTransaction.id(); + if (this.self.#_senderState?.transactionId === transactionId) { + return this.self.#_senderState.sender; + } else { + let sender = Provable.witness(PublicKey, () => Mina.sender()); + this.self.#_senderState = { transactionId, sender }; + return sender; + } + }, + + getAndRequireSignature(): PublicKey { + let sender = this.getUnconstrained(); + AccountUpdate.createSigned(sender); return sender; - } - } + }, + }; /** * Current account of the {@link SmartContract}. From 0a828d9a3ebbc4a64d2eded485bee9cbf2093fb8 Mon Sep 17 00:00:00 2001 From: julio4 Date: Wed, 14 Feb 2024 23:27:03 +0900 Subject: [PATCH 2/6] Fix: use safe sender getters in contract examples --- src/examples/zkapps/dex/dex-with-actions.ts | 23 ++++++++++++--------- src/examples/zkapps/dex/dex.ts | 22 ++++++++++++-------- src/examples/zkapps/simple-zkapp-payment.ts | 4 ++-- tests/integration/simple-zkapp.js | 3 ++- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/examples/zkapps/dex/dex-with-actions.ts b/src/examples/zkapps/dex/dex-with-actions.ts index add16ae08b..65fdf4c4b4 100644 --- a/src/examples/zkapps/dex/dex-with-actions.ts +++ b/src/examples/zkapps/dex/dex-with-actions.ts @@ -71,7 +71,7 @@ class Dex extends SmartContract { } @method createAccount() { - this.token.mint({ address: this.sender, amount: UInt64.from(0) }); + this.token.mint({ address: this.sender.getUnconstrained(), amount: UInt64.from(0) }); } /** @@ -85,7 +85,7 @@ class Dex extends SmartContract { * instead, the input X and Y amounts determine the initial ratio. */ @method supplyLiquidityBase(dx: UInt64, dy: UInt64): UInt64 { - let user = this.sender; + let user = this.sender.getUnconstrained(); let tokenX = new TokenContract(this.tokenX); let tokenY = new TokenContract(this.tokenY); @@ -156,14 +156,15 @@ class Dex extends SmartContract { * contracts pay you tokens when reducing the action. */ @method redeemInitialize(dl: UInt64) { - this.reducer.dispatch(new RedeemAction({ address: this.sender, dl })); - this.token.burn({ address: this.sender, amount: dl }); + let sender = this.sender.getAndRequireSignature(); + this.reducer.dispatch(new RedeemAction({ address: sender, dl })); + this.token.burn({ address: sender, amount: dl }); // TODO: preconditioning on the state here ruins concurrent interactions, // there should be another `finalize` DEX method which reduces actions & updates state this.totalSupply.set(this.totalSupply.getAndRequireEquals().sub(dl)); // emit event - this.typedEvents.emit('redeem-liquidity', { address: this.sender, dl }); + this.typedEvents.emit('redeem-liquidity', { address: sender, dl }); } /** @@ -186,10 +187,11 @@ class Dex extends SmartContract { * the called methods which requires proof authorization. */ swapX(dx: UInt64): UInt64 { + let sender = this.sender.getAndRequireSignature(); let tokenY = new TokenContract(this.tokenY); let dexY = new DexTokenHolder(this.address, tokenY.token.id); - let dy = dexY.swap(this.sender, dx, this.tokenX); - tokenY.transfer(dexY.self, this.sender, dy); + let dy = dexY.swap(sender, dx, this.tokenX); + tokenY.transfer(dexY.self, sender, dy); return dy; } @@ -204,10 +206,11 @@ class Dex extends SmartContract { * the called methods which requires proof authorization. */ swapY(dy: UInt64): UInt64 { + let sender = this.sender.getAndRequireSignature(); let tokenX = new TokenContract(this.tokenX); let dexX = new DexTokenHolder(this.address, tokenX.token.id); - let dx = dexX.swap(this.sender, dy, this.tokenY); - tokenX.transfer(dexX.self, this.sender, dx); + let dx = dexX.swap(sender, dy, this.tokenY); + tokenX.transfer(dexX.self, sender, dx); return dx; } @@ -308,7 +311,7 @@ class DexTokenHolder extends SmartContract { this.balance.subInPlace(dy); // emit event - this.typedEvents.emit('swap', { address: this.sender, dx }); + this.typedEvents.emit('swap', { address: this.sender.getAndRequireSignature(), dx }); return dy; } diff --git a/src/examples/zkapps/dex/dex.ts b/src/examples/zkapps/dex/dex.ts index 5799e6f80a..ac71b8a531 100644 --- a/src/examples/zkapps/dex/dex.ts +++ b/src/examples/zkapps/dex/dex.ts @@ -48,7 +48,7 @@ function createDex({ * instead, the input X and Y amounts determine the initial ratio. */ @method supplyLiquidityBase(dx: UInt64, dy: UInt64): UInt64 { - let user = this.sender; + let user = this.sender.getAndRequireSignature(); let tokenX = new TokenContract(this.tokenX); let tokenY = new TokenContract(this.tokenY); @@ -135,11 +135,12 @@ function createDex({ */ redeemLiquidity(dl: UInt64) { // call the token X holder inside a token X-approved callback + let sender = this.sender.getAndRequireSignature(); let tokenX = new TokenContract(this.tokenX); let dexX = new DexTokenHolder(this.address, tokenX.token.id); - let dxdy = dexX.redeemLiquidity(this.sender, dl, this.tokenY); + let dxdy = dexX.redeemLiquidity(sender, dl, this.tokenY); let dx = dxdy[0]; - tokenX.transfer(dexX.self, this.sender, dx); + tokenX.transfer(dexX.self, sender, dx); return dxdy; } @@ -151,10 +152,11 @@ function createDex({ * The transaction needs to be signed by the user's private key. */ @method swapX(dx: UInt64): UInt64 { + let sender = this.sender.getAndRequireSignature(); let tokenY = new TokenContract(this.tokenY); let dexY = new DexTokenHolder(this.address, tokenY.token.id); - let dy = dexY.swap(this.sender, dx, this.tokenX); - tokenY.transfer(dexY.self, this.sender, dy); + let dy = dexY.swap(sender, dx, this.tokenX); + tokenY.transfer(dexY.self, sender, dy); return dy; } @@ -166,10 +168,11 @@ function createDex({ * The transaction needs to be signed by the user's private key. */ @method swapY(dy: UInt64): UInt64 { + let sender = this.sender.getAndRequireSignature(); let tokenX = new TokenContract(this.tokenX); let dexX = new DexTokenHolder(this.address, tokenX.token.id); - let dx = dexX.swap(this.sender, dy, this.tokenY); - tokenX.transfer(dexX.self, this.sender, dx); + let dx = dexX.swap(sender, dy, this.tokenY); + tokenX.transfer(dexX.self, sender, dx); return dx; } @@ -200,10 +203,11 @@ function createDex({ class ModifiedDex extends Dex { @method swapX(dx: UInt64): UInt64 { + let sender = this.sender.getAndRequireSignature(); let tokenY = new TokenContract(this.tokenY); let dexY = new ModifiedDexTokenHolder(this.address, tokenY.token.id); - let dy = dexY.swap(this.sender, dx, this.tokenX); - tokenY.transfer(dexY.self, this.sender, dy); + let dy = dexY.swap(sender, dx, this.tokenX); + tokenY.transfer(dexY.self, sender, dy); return dy; } } diff --git a/src/examples/zkapps/simple-zkapp-payment.ts b/src/examples/zkapps/simple-zkapp-payment.ts index 2fe6f4d215..598d98ea0d 100644 --- a/src/examples/zkapps/simple-zkapp-payment.ts +++ b/src/examples/zkapps/simple-zkapp-payment.ts @@ -22,11 +22,11 @@ class SendMINAExample extends SmartContract { } @method withdraw(amount: UInt64) { - this.send({ to: this.sender, amount }); + this.send({ to: this.sender.getAndRequireSignature(), amount }); } @method deposit(amount: UInt64) { - let senderUpdate = AccountUpdate.createSigned(this.sender); + let senderUpdate = AccountUpdate.createSigned(this.sender.getAndRequireSignature()); senderUpdate.send({ to: this, amount }); } } diff --git a/tests/integration/simple-zkapp.js b/tests/integration/simple-zkapp.js index 5df6113c3d..f083a56aaf 100644 --- a/tests/integration/simple-zkapp.js +++ b/tests/integration/simple-zkapp.js @@ -61,7 +61,8 @@ class NotSoSimpleZkapp extends SmartContract { } deposit(amount) { - let senderUpdate = AccountUpdate.createSigned(this.sender); + let sender = this.sender.getAndRequireSignature(); + let senderUpdate = AccountUpdate.createSigned(sender); senderUpdate.send({ to: this, amount }); } } From a16b5737f8e49874b5708bb0544ce19737b59f07 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 7 Mar 2024 12:23:50 +0100 Subject: [PATCH 3/6] simplify type --- src/lib/zkapp.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lib/zkapp.ts b/src/lib/zkapp.ts index ffc3cbad60..ffba37a9da 100644 --- a/src/lib/zkapp.ts +++ b/src/lib/zkapp.ts @@ -821,12 +821,8 @@ super.init(); #_senderState: { sender: PublicKey; transactionId: number }; - sender: { - self: SmartContract; - getUnconstrained(): PublicKey; - getAndRequireSignature(): PublicKey; - } = { - self: this, + sender = { + self: this as SmartContract, /** * The public key of the current transaction's sender account. * From ccba9799b08d89860d0c2d072b962d4d79264251 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 7 Mar 2024 12:24:36 +0100 Subject: [PATCH 4/6] fix usage of sender, use unconstrained --- src/examples/zkapps/dex/dex-with-actions.ts | 26 ++++++++++++++------- src/examples/zkapps/dex/dex.ts | 24 +++++++++---------- src/examples/zkapps/simple-zkapp-payment.ts | 7 ++++-- tests/integration/simple-zkapp.js | 2 +- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/examples/zkapps/dex/dex-with-actions.ts b/src/examples/zkapps/dex/dex-with-actions.ts index a6e6135d07..b61eec6515 100644 --- a/src/examples/zkapps/dex/dex-with-actions.ts +++ b/src/examples/zkapps/dex/dex-with-actions.ts @@ -79,7 +79,11 @@ class Dex extends TokenContract { } @method createAccount() { - this.internal.mint({ address: this.sender, amount: UInt64.from(0) }); + this.internal.mint({ + // unconstrained because we don't care which account is created + address: this.sender.getUnconstrained(), + amount: UInt64.from(0), + }); } /** @@ -93,7 +97,8 @@ class Dex extends TokenContract { * instead, the input X and Y amounts determine the initial ratio. */ @method supplyLiquidityBase(dx: UInt64, dy: UInt64): UInt64 { - let user = this.sender; + // unconstrained because `transfer()` requires sender signature anyway + let user = this.sender.getUnconstrained(); let tokenX = new TrivialCoin(this.tokenX); let tokenY = new TrivialCoin(this.tokenY); @@ -164,8 +169,9 @@ class Dex extends TokenContract { * contracts pay you tokens when reducing the action. */ @method redeemInitialize(dl: UInt64) { - this.reducer.dispatch(new RedeemAction({ address: this.sender, dl })); - this.internal.burn({ address: this.sender, amount: dl }); + let sender = this.sender.getUnconstrained(); // unconstrained because `burn()` requires sender signature anyway + this.reducer.dispatch(new RedeemAction({ address: sender, dl })); + this.internal.burn({ address: sender, amount: dl }); // TODO: preconditioning on the state here ruins concurrent interactions, // there should be another `finalize` DEX method which reduces actions & updates state this.totalSupply.set(this.totalSupply.getAndRequireEquals().sub(dl)); @@ -194,10 +200,11 @@ class Dex extends TokenContract { * the called methods which requires proof authorization. */ swapX(dx: UInt64): UInt64 { + let user = this.sender.getUnconstrained(); // unconstrained because `swap()` requires sender signature anyway let tokenY = new TrivialCoin(this.tokenY); let dexY = new DexTokenHolder(this.address, tokenY.deriveTokenId()); - let dy = dexY.swap(this.sender, dx, this.tokenX); - tokenY.transfer(dexY.self, this.sender, dy); + let dy = dexY.swap(user, dx, this.tokenX); + tokenY.transfer(dexY.self, user, dy); return dy; } @@ -212,10 +219,11 @@ class Dex extends TokenContract { * the called methods which requires proof authorization. */ swapY(dy: UInt64): UInt64 { + let user = this.sender.getUnconstrained(); // unconstrained because `swap()` requires sender signature anyway let tokenX = new TrivialCoin(this.tokenX); let dexX = new DexTokenHolder(this.address, tokenX.deriveTokenId()); - let dx = dexX.swap(this.sender, dy, this.tokenY); - tokenX.transfer(dexX.self, this.sender, dx); + let dx = dexX.swap(user, dy, this.tokenY); + tokenX.transfer(dexX.self, user, dx); return dx; } } @@ -312,7 +320,7 @@ class DexTokenHolder extends SmartContract { this.balance.subInPlace(dy); // emit event - this.typedEvents.emit('swap', { address: this.sender.getAndRequireSignature(), dx }); + this.typedEvents.emit('swap', { address: user, dx }); return dy; } diff --git a/src/examples/zkapps/dex/dex.ts b/src/examples/zkapps/dex/dex.ts index 697edab4b8..57479e7304 100644 --- a/src/examples/zkapps/dex/dex.ts +++ b/src/examples/zkapps/dex/dex.ts @@ -55,7 +55,7 @@ function createDex({ * instead, the input X and Y amounts determine the initial ratio. */ @method supplyLiquidityBase(dx: UInt64, dy: UInt64): UInt64 { - let user = this.sender.getAndRequireSignature(); + let user = this.sender.getUnconstrained(); // unconstrained because transfer() requires the signature anyway let tokenX = new TokenContract(this.tokenX); let tokenY = new TokenContract(this.tokenY); @@ -148,10 +148,10 @@ function createDex({ */ redeemLiquidity(dl: UInt64) { // call the token X holder inside a token X-approved callback - let sender = this.sender.getAndRequireSignature(); + let sender = this.sender.getUnconstrained(); // unconstrained because redeemLiquidity() requires the signature anyway let tokenX = new TokenContract(this.tokenX); let dexX = new DexTokenHolder(this.address, tokenX.deriveTokenId()); - let dxdy = dexX.redeemLiquidity(this.sender, dl, this.tokenY); + let dxdy = dexX.redeemLiquidity(sender, dl, this.tokenY); let dx = dxdy[0]; tokenX.transfer(dexX.self, sender, dx); return dxdy; @@ -165,11 +165,11 @@ function createDex({ * The transaction needs to be signed by the user's private key. */ @method swapX(dx: UInt64): UInt64 { - let sender = this.sender.getAndRequireSignature(); + let sender = this.sender.getUnconstrained(); // unconstrained because swap() requires the signature anyway let tokenY = new TokenContract(this.tokenY); let dexY = new DexTokenHolder(this.address, tokenY.deriveTokenId()); - let dy = dexY.swap(this.sender, dx, this.tokenX); - tokenY.transfer(dexY.self, this.sender, dy); + let dy = dexY.swap(sender, dx, this.tokenX); + tokenY.transfer(dexY.self, sender, dy); return dy; } @@ -181,11 +181,11 @@ function createDex({ * The transaction needs to be signed by the user's private key. */ @method swapY(dy: UInt64): UInt64 { - let sender = this.sender.getAndRequireSignature(); + let sender = this.sender.getUnconstrained(); // unconstrained because swap() requires the signature anyway let tokenX = new TokenContract(this.tokenX); let dexX = new DexTokenHolder(this.address, tokenX.deriveTokenId()); - let dx = dexX.swap(this.sender, dy, this.tokenY); - tokenX.transfer(dexX.self, this.sender, dx); + let dx = dexX.swap(sender, dy, this.tokenY); + tokenX.transfer(dexX.self, sender, dx); return dx; } @@ -218,14 +218,14 @@ function createDex({ } @method swapX(dx: UInt64): UInt64 { - let sender = this.sender.getAndRequireSignature(); + let sender = this.sender.getUnconstrained(); // unconstrained because swap() requires the signature anyway let tokenY = new TokenContract(this.tokenY); let dexY = new ModifiedDexTokenHolder( this.address, tokenY.deriveTokenId() ); - let dy = dexY.swap(this.sender, dx, this.tokenX); - tokenY.transfer(dexY.self, this.sender, dy); + let dy = dexY.swap(sender, dx, this.tokenX); + tokenY.transfer(dexY.self, sender, dy); return dy; } } diff --git a/src/examples/zkapps/simple-zkapp-payment.ts b/src/examples/zkapps/simple-zkapp-payment.ts index 0594a01728..2995f10451 100644 --- a/src/examples/zkapps/simple-zkapp-payment.ts +++ b/src/examples/zkapps/simple-zkapp-payment.ts @@ -22,11 +22,14 @@ class SendMINAExample extends SmartContract { } @method withdraw(amount: UInt64) { - this.send({ to: this.sender.getAndRequireSignature(), amount }); + // unconstrained because we don't care where the user wants to withdraw to + let to = this.sender.getUnconstrained(); + this.send({ to, amount }); } @method deposit(amount: UInt64) { - let senderUpdate = AccountUpdate.createSigned(this.sender.getAndRequireSignature()); + let sender = this.sender.getUnconstrained(); // unconstrained because we're already requiring a signature in the next line + let senderUpdate = AccountUpdate.createSigned(sender); senderUpdate.send({ to: this, amount }); } } diff --git a/tests/integration/simple-zkapp.js b/tests/integration/simple-zkapp.js index 86df100643..bea66cc0ba 100644 --- a/tests/integration/simple-zkapp.js +++ b/tests/integration/simple-zkapp.js @@ -61,7 +61,7 @@ class NotSoSimpleZkapp extends SmartContract { } deposit(amount) { - let sender = this.sender.getAndRequireSignature(); + let sender = this.sender.getUnconstrained(); // unconstrained because we're already requiring a signature in the next line let senderUpdate = AccountUpdate.createSigned(sender); senderUpdate.send({ to: this, amount }); } From 348ea1e9b6535633df821dda79a642f165acc4a5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 7 Mar 2024 12:54:16 +0100 Subject: [PATCH 5/6] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18c72e73ba..ffdcb91508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `Provable.runAndCheck()`, `Provable.constraintSystem()` and `{SmartContract,ZkProgram}.digest()` are also async now - These changes were made to add internal support for async circuits - `Provable.runAndCheckSync()` added and immediately deprecated for a smoother upgrade path for tests +- Remove `this.sender` which unintuitively did not prove that its value was the actual sender of the transaction https://github.com/o1-labs/o1js/pull/1464 + Replaced by more explicit APIs: + - `this.sender.getUnconstrained()` which has the old behavior of `this.sender`, and returns an unconstrained value (which means that the prover can set it to any value they want) + - `this.sender.getAndRequireSignature()` which requires a signature from the sender's public key and therefore proves that whoever created the transaction really owns the sender account - `Reducer.reduce()` requires the maximum number of actions per method as an explicit (optional) argument https://github.com/o1-labs/o1js/pull/1450 - The default value is 1 and should work for most existing contracts From d65902af6b5036fb264238b2a6f922d5a1030e44 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 7 Mar 2024 12:55:45 +0100 Subject: [PATCH 6/6] tweak changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffdcb91508..2539232e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - `Provable.runAndCheck()`, `Provable.constraintSystem()` and `{SmartContract,ZkProgram}.digest()` are also async now - These changes were made to add internal support for async circuits - `Provable.runAndCheckSync()` added and immediately deprecated for a smoother upgrade path for tests -- Remove `this.sender` which unintuitively did not prove that its value was the actual sender of the transaction https://github.com/o1-labs/o1js/pull/1464 +- Remove `this.sender` which unintuitively did not prove that its value was the actual sender of the transaction https://github.com/o1-labs/o1js/pull/1464 [@julio4](https://github.com/julio4) Replaced by more explicit APIs: - `this.sender.getUnconstrained()` which has the old behavior of `this.sender`, and returns an unconstrained value (which means that the prover can set it to any value they want) - `this.sender.getAndRequireSignature()` which requires a signature from the sender's public key and therefore proves that whoever created the transaction really owns the sender account