Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement stubs for LogicSig and arg op code #16

Merged
merged 3 commits into from
Dec 22, 2024
Merged
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
39 changes: 39 additions & 0 deletions examples/htlc-logicsig/signature.algo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Account, Bytes, Global, LogicSig, op, TransactionType, Txn, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
import algosdk from 'algosdk'

export default class HashedTimeLockedLogicSig extends LogicSig {
program(): boolean | uint64 {
// Participants
const sellerAddress = Bytes(algosdk.decodeAddress('6ZHGHH5Z5CTPCF5WCESXMGRSVK7QJETR63M3NY5FJCUYDHO57VTCMJOBGY').publicKey)
const buyerAddress = Bytes(algosdk.decodeAddress('7Z5PWO2C6LFNQFGHWKSK5H47IQP5OJW2M3HA2QPXTY3WTNP5NU2MHBW27M').publicKey)
const seller = Account(sellerAddress)
const buyer = Account(buyerAddress)

// Contract parameters
const feeLimit = Uint64(1000)
const secretHash = Bytes.fromHex('2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b')
const timeout = Uint64(3000)

// Transaction conditions
const isPayment = Txn.typeEnum === TransactionType.Payment
const isFeeAcceptable = Txn.fee < feeLimit
const isNoCloseTo = Txn.closeRemainderTo === Global.zeroAddress
const isNoRekey = Txn.rekeyTo === Global.zeroAddress

// Safety conditions
const safetyConditions = isPayment && isNoCloseTo && isNoRekey

// Seller receives payment if correct secret is provided
const isToSeller = Txn.receiver === seller
const isSecretCorrect = op.sha256(op.arg(0)) === secretHash
const sellerReceives = isToSeller && isSecretCorrect

// Buyer receives refund after timeout
const isToBuyer = Txn.receiver === buyer
const isAfterTimeout = Txn.firstValid > timeout
const buyerReceivesRefund = isToBuyer && isAfterTimeout

// Final contract logic
return isFeeAcceptable && safetyConditions && (sellerReceives || buyerReceivesRefund)
}
}
32 changes: 32 additions & 0 deletions examples/htlc-logicsig/signature.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Account, Bytes } from '@algorandfoundation/algorand-typescript'
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
import algosdk from 'algosdk'
import { afterEach, describe, expect, it } from 'vitest'
import { ZERO_ADDRESS } from '../../src/constants'
import HashedTimeLockedLogicSig from './signature.algo'

describe('HTLC LogicSig', () => {
const ctx = new TestExecutionContext()

afterEach(() => {
ctx.reset()
})

it('seller receives payment if correct secret is provided', () => {
const receiverAddress = Bytes(algosdk.decodeAddress('6ZHGHH5Z5CTPCF5WCESXMGRSVK7QJETR63M3NY5FJCUYDHO57VTCMJOBGY').publicKey)
ctx.txn
.createScope([
ctx.any.txn.payment({
fee: 500,
firstValid: 1000,
closeRemainderTo: Account(ZERO_ADDRESS),
rekeyTo: Account(ZERO_ADDRESS),
receiver: Account(receiverAddress),
}),
])
.execute(() => {
const result = ctx.executeLogicSig(new HashedTimeLockedLogicSig(), Bytes('secret'))
expect(result).toBe(true)
})
})
})
1 change: 1 addition & 0 deletions examples/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const config: RollupOptions = {
'examples/auction/contract.algo.ts',
'examples/voting/contract.algo.ts',
'examples/simple-voting/contract.algo.ts',
'examples/zk-whitelist/contract.algo.ts',
],
output: [
{
Expand Down
94 changes: 94 additions & 0 deletions examples/zk-whitelist/contract.algo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
abimethod,
Account,
arc4,
assert,
BigUint,
Bytes,
ensureBudget,
Global,
itxn,
LocalState,
op,
OpUpFeeSource,
TemplateVar,
Txn,
uint64,
} from '@algorandfoundation/algorand-typescript'

const curveMod = 21888242871839275222246405745257275088548364400416034343698204186575808495617n
const verifierBudget = 145000

export default class ZkWhitelistContract extends arc4.Contract {
appName: arc4.Str | undefined
whiteList = LocalState<boolean>()

@abimethod({ onCreate: 'require' })
create(name: arc4.Str) {
// Create the application
this.appName = name
}

@abimethod({ allowActions: ['UpdateApplication', 'DeleteApplication'] })
update() {
// Update the application if it is mutable (manager only)
assert(Global.creatorAddress === Txn.sender)
}

@abimethod({ allowActions: ['OptIn', 'CloseOut'] })
optInOrOut() {
// Opt in or out of the application
return
}

@abimethod()
addAddressToWhitelist(address: arc4.Address, proof: arc4.DynamicArray<arc4.Address>): arc4.Str {
/*
Add caller to the whitelist if the zk proof is valid.
On success, will return an empty string. Otherwise, will return an error
message.
*/
ensureBudget(verifierBudget, OpUpFeeSource.GroupCredit)
// The verifier expects public inputs to be in the curve field, but an
// Algorand address might represent a number larger than the field
// modulus, so to be safe we take the address modulo the field modulus
const addressMod = arc4.interpretAsArc4<arc4.Address>(op.bzero(32).bitwiseOr(Bytes(BigUint(address.bytes) % curveMod)))
// Verify the proof by calling the deposit verifier app
const verified = this.verifyProof(TemplateVar<uint64>('VERIFIER_APP_ID'), proof, new arc4.DynamicArray(addressMod))
if (!verified.native) {
return new arc4.Str('Proof verification failed')
}
// if successful, add the sender to the whitelist by setting local state
const account = Account(address.bytes)
if (Txn.sender !== account) {
return new arc4.Str('Sender address does not match authorized address')
}
this.whiteList(account).value = true
return new arc4.Str('')
}

@abimethod()
isOnWhitelist(address: arc4.Address): arc4.Bool {
// Check if an address is on the whitelist
const account = address.native
const optedIn = op.appOptedIn(account, Global.currentApplicationId)
if (!optedIn) {
return new arc4.Bool(false)
}
const whitelisted = this.whiteList(account).value
return new arc4.Bool(whitelisted)
}

verifyProof(appId: uint64, proof: arc4.DynamicArray<arc4.Address>, publicInputs: arc4.DynamicArray<arc4.Address>): arc4.Bool {
// Verify a proof using the verifier app.
const verified = itxn
.applicationCall({
appId: appId,
fee: 0,
appArgs: [arc4.methodSelector('verify(byte[32][],byte[32][])bool'), proof.copy(), publicInputs.copy()],
onCompletion: arc4.OnCompleteAction.NoOp,
})
.submit().lastLog
return arc4.interpretAsArc4<arc4.Bool>(verified, 'log')
}
}
79 changes: 79 additions & 0 deletions examples/zk-whitelist/contract.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { arc4, Bytes } from '@algorandfoundation/algorand-typescript'
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
import { afterEach, describe, expect, it } from 'vitest'
import { ABI_RETURN_VALUE_LOG_PREFIX } from '../../src/constants'
import ZkWhitelistContract from './contract.algo'

describe('ZK Whitelist', () => {
const ctx = new TestExecutionContext()

afterEach(() => {
ctx.reset()
})

it('should be able to add address to whitelist', () => {
// Arrange
const contract = ctx.contract.create(ZkWhitelistContract)
contract.create(ctx.any.arc4.str(10))

const address = new arc4.Address(ctx.defaultSender)
const proof = new arc4.DynamicArray<arc4.Address>(new arc4.Address(Bytes(new Uint8Array(Array(32).fill(0)))))

const dummyVerifierApp = ctx.any.application({ appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(Bytes.fromHex('80'))] })
ctx.setTemplateVar('VERIFIER_APP_ID', dummyVerifierApp.id)

// Act
const result = contract.addAddressToWhitelist(address, proof)

// Assert
expect(result.native).toEqual('')
expect(contract.whiteList(ctx.defaultSender).value).toEqual(true)
})

it('returns error message if proof verification fails', () => {
// Arrange
const contract = ctx.contract.create(ZkWhitelistContract)
contract.create(ctx.any.arc4.str(10))

const address = ctx.any.arc4.address()
const proof = new arc4.DynamicArray<arc4.Address>(new arc4.Address(Bytes(new Uint8Array(Array(32).fill(0)))))
const dummyVerifierApp = ctx.any.application({ appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(Bytes(''))] })
ctx.setTemplateVar('VERIFIER_APP_ID', dummyVerifierApp.id)

// Act
const result = contract.addAddressToWhitelist(address, proof)

// Assert
expect(result.native).toEqual('Proof verification failed')
})

it('returns true if address is already in whitelist', () => {
// Arrange
const contract = ctx.contract.create(ZkWhitelistContract)
contract.create(ctx.any.arc4.str(10))

const dummyAccount = ctx.any.account({ optedApplications: [ctx.ledger.getApplicationForContract(contract)] })
contract.whiteList(dummyAccount).value = true

// Act
const result = contract.isOnWhitelist(new arc4.Address(dummyAccount))

// Assert
expect(result.native).toBe(true)
})

it('returns false if address is not in whitelist', () => {
// Arrange
const contract = ctx.contract.create(ZkWhitelistContract)
contract.create(ctx.any.arc4.str(10))

const dummyAccount = ctx.any.account({ optedApplications: [ctx.ledger.getApplicationForContract(contract)] })
contract.whiteList(dummyAccount).value = false

// Act
const result = contract.isOnWhitelist(new arc4.Address(dummyAccount))

// Assert
expect(result.native).toBe(false)
})
})
11 changes: 6 additions & 5 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@
"tslib": "^2.6.2"
},
"dependencies": {
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.22",
"@algorandfoundation/puya-ts": "^1.0.0-alpha.34",
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.23",
"@algorandfoundation/puya-ts": "^1.0.0-alpha.35",
"elliptic": "^6.5.7",
"js-sha256": "^0.11.0",
"js-sha3": "^0.9.3",
Expand Down
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { Bytes } from '@algorandfoundation/algorand-typescript'
export const UINT64_SIZE = 64
export const UINT512_SIZE = 512
export const MAX_UINT8 = 2 ** 8 - 1
export const MAX_UINT16 = 2 ** 16 - 1
export const MAX_UINT32 = 2 ** 32 - 1
export const MAX_UINT64 = 2n ** 64n - 1n
export const MAX_UINT128 = 2n ** 128n - 1n
export const MAX_UINT256 = 2n ** 256n - 1n
export const MAX_UINT512 = 2n ** 512n - 1n
export const MAX_BYTES_SIZE = 4096
export const MAX_LOG_SIZE = 1024
Expand Down Expand Up @@ -43,6 +47,7 @@ export const ABI_RETURN_VALUE_LOG_PREFIX = Bytes.fromHex('151F7C75')

export const UINT64_OVERFLOW_UNDERFLOW_MESSAGE = 'Uint64 overflow or underflow'
export const BIGUINT_OVERFLOW_UNDERFLOW_MESSAGE = 'BigUint overflow or underflow'
export const DEFAULT_TEMPLATE_VAR_PREFIX = 'TMPL_'

export const APP_ID_PREFIX = 'appID'
export const HASH_BYTES_LENGTH = 32
Expand Down
16 changes: 15 additions & 1 deletion src/impl/acct-params.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Account, gtxn, internal, uint64 } from '@algorandfoundation/algorand-typescript'
import { Account, Application, gtxn, internal, uint64 } from '@algorandfoundation/algorand-typescript'
import { lazyContext } from '../context-helpers/internal-context'
import { asMaybeUint64Cls } from '../util'
import { getApp } from './app-params'

export const getAccount = (acct: Account | internal.primitives.StubUint64Compat): Account => {
const acctId = asMaybeUint64Cls(acct)
Expand All @@ -21,6 +22,19 @@ export const minBalance = (a: Account | internal.primitives.StubUint64Compat): u
return acct.minBalance
}

export const appOptedIn = (
a: Account | internal.primitives.StubUint64Compat,
b: Application | internal.primitives.StubUint64Compat,
): boolean => {
const account = getAccount(a)
const app = getApp(b)

if (account === undefined || app === undefined) {
return false
}
return account.isOptedIn(app)
}

export const AcctParams: internal.opTypes.AcctParamsType = {
acctBalance(a: Account | internal.primitives.StubUint64Compat): readonly [uint64, boolean] {
const acct = getAccount(a)
Expand Down
5 changes: 5 additions & 0 deletions src/impl/ensure-budget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { OpUpFeeSource, uint64 } from '@algorandfoundation/algorand-typescript'

export function ensureBudgetImpl(_budget: uint64, _feeSource: OpUpFeeSource = OpUpFeeSource.GroupCredit) {
// ensureBudget function is emulated to be a no-op
}
9 changes: 5 additions & 4 deletions src/impl/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
export { AcctParams, balance, minBalance } from './acct-params'
export { AcctParams, appOptedIn, balance, minBalance } from './acct-params'
export { AppGlobal } from './app-global'
export { AppLocal } from './app-local'
export { AppParams } from './app-params'
export { AssetHolding } from './asset-holding'
export { AssetParams } from './asset-params'
export { Block } from './block'
export { Box } from './box'
export * from './crypto'
export { Global } from './global'
export { GTxn } from './gtxn'
export { GITxn, ITxn, ITxnCreate } from './itxn'
export { arg } from './logicSigArg'
export * from './pure'
export { Scratch, gloadBytes, gloadUint64 } from './scratch'
export { Block } from './block'
export { Txn, gaid } from './txn'
export { gloadBytes, gloadUint64, Scratch } from './scratch'
export { gaid, Txn } from './txn'
8 changes: 8 additions & 0 deletions src/impl/logicSigArg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { bytes, internal } from '@algorandfoundation/algorand-typescript'
import { lazyContext } from '../context-helpers/internal-context'
import { asNumber } from '../util'

export const arg = (a: internal.primitives.StubUint64Compat): bytes => {
const index = asNumber(a)
return lazyContext.value.activeLogicSigArgs[index]
}
Loading
Loading