diff --git a/CHANGELOG.md b/CHANGELOG.md index c74c9e8..a0dfd10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ ### Compatibility and validation +- Extend the live overload entry to root-signed AA batches for both ML-DSA-65 + and Dilithium3 across canonical, inherited, internal, free-function and array + forms. Verify tampered-data/signature/algorithm rejection, later-call atomic + rollback, mined state and outer fees/nonces, live P2P import, historical reads, + offline catch-up and finalized crash recovery followed by a new batch. + Node source remains `e94038717a1522080ecf701edacbde21cf37b870`; genesis + predeployment is explicit. Native CREATE, CLI contract-write entries and + matching public package/profile acceptance remain separate. + - Verify inherited, internal, free-function and array overloads through their original selectors on a real isolated memory node, including full-word address/MAX versus integer results, SDK ABI codecs and exact rejection data. @@ -12,8 +21,8 @@ rollback, fee/nonce and historical getters, independent P2P follower import, offline catch-up and finalized RocksDB crash recovery with continued writes. Checked against shell-chain source `e94038717a1522080ecf701edacbde21cf37b870`. - These isolated dev cases use genesis predeployment; SDK live AA, native CREATE - and matching public package/profile acceptance remain open. + These isolated dev cases use genesis predeployment; native CREATE, CLI + contract-write entries and matching public package/profile acceptance remain open. - Extend the canonical overload RPC regression through real ML-DSA mined writes, rollback, fee/nonce and history checks, P2P follower import, and RocksDB crash diff --git a/README.md b/README.md index 949924e..cc62044 100644 --- a/README.md +++ b/README.md @@ -1187,8 +1187,11 @@ SHELL_NODE_BIN=/path/to/shell-node node --test --test-name-pattern='canonical ov It checks the original scalar overloads and inherited, internal, free-function and array compositions through genuinely signed ML-DSA-65 and Dilithium3 -writes. The original ABI is retained, and fixed selectors and manually encoded -words check SDK calldata. Assertions cover mined receipts and state, +ordinary writes and root-signed AA batches. AA cases reject tampered inner +calldata, corrupt signatures and a mismatched algorithm identifier, and verify +that a later failing call rolls back earlier writes in the same batch while the +outer fee and nonce still apply. The original ABI is retained, and fixed +selectors and manually encoded words check SDK calldata. Assertions cover mined receipts and state, zero/MAX/truncated-input rollback, empty-array rejection, independent fee and nonce accounting, historical queries, P2P follower import, offline follower catch-up, and crash recovery with a new write after restart. diff --git a/tests/contracts.test.mjs b/tests/contracts.test.mjs index d8e7b39..2df7ddb 100644 --- a/tests/contracts.test.mjs +++ b/tests/contracts.test.mjs @@ -17,6 +17,7 @@ import { import { compileSolidity } from '../dist/contracts-compiler.js'; import { createShellProvider } from '../dist/provider.js'; import { acknowledgementHash } from './helpers.mjs'; +import { buildBatchTransaction } from '../dist/transactions.js'; const ADDRESS = '0x' + '11'.repeat(32); const CONTRACT = '0x' + '22'.repeat(32); @@ -669,7 +670,8 @@ test('read and write helpers share explicit original overload selection', async const derivedLiveCases=JSON.parse(readFileSync(new URL('./fixtures/native-derived-overloads.json',import.meta.url),'utf8')); for(const variant of [{kind:'canonical',array:false},...derivedLiveCases]) { for(const algorithm of ['MlDsa65','Dilithium3']) { - test(`canonical overload real RPC: ${variant.kind}/${algorithm} mined state, P2P import and crash recovery`, + for(const entry of ['ordinary','AA']) { + test(`canonical overload real RPC: ${variant.kind}/${algorithm}/${entry} mined state, P2P import and crash recovery`, {skip:!process.env.SHELL_NODE_BIN, timeout:120000}, async () => { const {mkdtemp,mkdir,writeFile,readFile,rm}=await import('node:fs/promises'); const {openSync,closeSync}=await import('node:fs'); @@ -682,7 +684,7 @@ for(const variant of [{kind:'canonical',array:false},...derivedLiveCases]) { const {generateAdapter}=await import('../dist/adapters.js'); const {ShellSigner}=await import('../dist/signer.js'); const binary=process.env.SHELL_NODE_BIN; - const directory=await mkdtemp(join(tmpdir(),`shell-sdk-overload-${variant.kind}-${algorithm}-`)); + const directory=await mkdtemp(join(tmpdir(),`shell-sdk-overload-${variant.kind}-${algorithm}-${entry}-`)); const signer=new ShellSigner(algorithm,generateAdapter(algorithm)); const contract='0x'+'99'.repeat(32), zero='0x'+'00'.repeat(32); const second='0x'+'56'.repeat(12)+'12'.repeat(20); @@ -816,7 +818,7 @@ contract StatefulOverloads { await start('follower',validator.bootnode); await until(async()=>Number(await rpc('follower','net_peerCount'))>=1,'follower peer'); // Independent expectations from the original Solidity state transitions. - const steps=[ + const ordinarySteps=[ ['set(address)',OVERLOAD_WORD,true,[OVERLOAD_WORD,zero,word(1)]], ['set(uint256)',OVERLOAD_AMOUNT,true,[OVERLOAD_WORD,word(OVERLOAD_AMOUNT+1n),word(2)]], ['set(address)',second,true,[second,word(OVERLOAD_AMOUNT+1n),word(3)]], @@ -826,29 +828,71 @@ contract StatefulOverloads { ...(variant.array?[['empty',null,false,[second,word(OVERLOAD_AMOUNT+1n),word(3)]]]:[]), ['set(uint256)',0n,true,[second,word(1),word(4)]], ]; + const aaSteps=[ + [[['set(address)',OVERLOAD_WORD],['set(uint256)',OVERLOAD_AMOUNT]],true,[OVERLOAD_WORD,word(OVERLOAD_AMOUNT+1n),word(2)]], + [[['set(address)',second]],true,[second,word(OVERLOAD_AMOUNT+1n),word(3)]], + [[['set(uint256)',0n],['set(address)',zero]],false,[second,word(OVERLOAD_AMOUNT+1n),word(3)]], + [[['set(address)',OVERLOAD_WORD],['set(uint256)',(1n<<256n)-1n]],false,[second,word(OVERLOAD_AMOUNT+1n),word(3)]], + [[['set(uint256)',23n],['truncated',OVERLOAD_WORD]],false,[second,word(OVERLOAD_AMOUNT+1n),word(3)]], + ...(variant.array?[[[['set(address)',OVERLOAD_WORD],['empty',null]],false,[second,word(OVERLOAD_AMOUNT+1n),word(3)]]]:[]), + [[['set(uint256)',0n],['set(address)',OVERLOAD_WORD]],true,[OVERLOAD_WORD,word(1),word(5)]], + ]; + const steps=entry==='AA'?aaSteps:ordinarySteps; async function execute(step,nonce,checkFollower=true) { - const [functionName,argument,success,words]=step; + const [functionName,argument]=step; + const success=step[entry==='AA'?1:2],words=step[entry==='AA'?2:3]; let hash; - const signature=variant.array?functionName.replace('address)','address[])').replace('uint256)','uint256[])'):functionName; - const args=[variant.array?[argument,typeof argument==='bigint'?23n:second]:argument]; - const addressSignature=variant.array?'set(address[])':'set(address)'; - if(functionName==='truncated'||functionName==='empty') { - const tx=buildContractCallTransaction({address:contract,abi,functionName:addressSignature,args:functionName==='empty'?[[]]:args, - chainId:1337,nonce,gasLimit:500000,maxFeePerGas:Number(maxFee),maxPriorityFeePerGas:0}); - if(functionName==='truncated')tx.data=tx.data.slice(0,-2); - hash=await validator.provider.sendTransaction(await signer.buildSignedTransaction({tx,includePublicKey:true})); + if(entry==='AA') { + const innerCalls=functionName.map(([name,value])=>{ + const signature=variant.array?name.replace('address)','address[])').replace('uint256)','uint256[])'):name; + const addressSignature=variant.array?'set(address[])':'set(address)'; + const args=[variant.array?[value,typeof value==='bigint'?23n:second]:value]; + let data=encodeFunctionData({abi,functionName:name==='truncated'||name==='empty'?addressSignature:signature,args:name==='empty'?[[]]:args}); + if(name==='truncated')data=data.slice(0,-2); + else if(name!=='empty') { + const selector=signature==='set(address)'?'0x2801617e':signature==='set(uint256)'?'0x60fe47b1':signature==='set(address[])'?'0x11f2a107':'0x6ea9bfc5'; + assert.equal(data,selector+(variant.array?word(32).slice(2)+word(2).slice(2):'')+word(value).slice(2) + +(variant.array?word(typeof value==='bigint'?23n:second).slice(2):'')); + } + return {to:contract,value:'0x0',data,gas_limit:'0x7a120'}; + }); + const {tx,aa_bundle}=buildBatchTransaction({chainId:1337,nonce,innerCalls,gasLimit:1500000, + maxFeePerGas:Number(maxFee),maxPriorityFeePerGas:0}); + const signed=await signer.buildSignedTransaction({tx,aaBundle:aa_bundle,includePublicKey:true}); + if(nonce===0) { + const before=await state('validator'); + const changed=structuredClone(signed);changed.aa_bundle.inner_calls[0].data=changed.aa_bundle.inner_calls[0].data.slice(0,-2)+'ff'; + await assert.rejects(nodes.validator.provider.sendTransaction(changed),/signature|hash|invalid|mismatch/i); + const corrupt=structuredClone(signed);corrupt.signature.data[0]^=1; + await assert.rejects(nodes.validator.provider.sendTransaction(corrupt),/signature|hash|invalid|mismatch/i); + const cross=structuredClone(signed);cross.signature.sig_type=algorithm==='MlDsa65'?'Dilithium3':'MlDsa65'; + await assert.rejects(nodes.validator.provider.sendTransaction(cross),/signature|hash|invalid|mismatch|sender|address/i); + assert.deepEqual(await state('validator'),before); + } + hash=await nodes.validator.provider.sendTransaction(signed); } else { - const selector=signature==='set(address)'?'0x2801617e':signature==='set(uint256)'?'0x60fe47b1':signature==='set(address[])'?'0x11f2a107':'0x6ea9bfc5'; - const expectedData=selector+(variant.array?word(32).slice(2)+word(2).slice(2):'')+word(argument).slice(2) - +(variant.array?word(typeof argument==='bigint'?23n:second).slice(2):''); - assert.equal(encodeFunctionData({abi,functionName:signature,args}),expectedData); - ({hash}=await writeContract({provider:nodes.validator.provider,signer,chainId:1337,nonce,address:contract, - abi,functionName:signature,args,includePublicKey:true,wait:false,gasLimit:500000, - maxFeePerGas:Number(maxFee),maxPriorityFeePerGas:0})); + const signature=variant.array?functionName.replace('address)','address[])').replace('uint256)','uint256[])'):functionName; + const args=[variant.array?[argument,typeof argument==='bigint'?23n:second]:argument]; + const addressSignature=variant.array?'set(address[])':'set(address)'; + if(functionName==='truncated'||functionName==='empty') { + const tx=buildContractCallTransaction({address:contract,abi,functionName:addressSignature,args:functionName==='empty'?[[]]:args, + chainId:1337,nonce,gasLimit:500000,maxFeePerGas:Number(maxFee),maxPriorityFeePerGas:0}); + if(functionName==='truncated')tx.data=tx.data.slice(0,-2); + hash=await validator.provider.sendTransaction(await signer.buildSignedTransaction({tx,includePublicKey:true})); + } else { + const selector=signature==='set(address)'?'0x2801617e':signature==='set(uint256)'?'0x60fe47b1':signature==='set(address[])'?'0x11f2a107':'0x6ea9bfc5'; + const expectedData=selector+(variant.array?word(32).slice(2)+word(2).slice(2):'')+word(argument).slice(2) + +(variant.array?word(typeof argument==='bigint'?23n:second).slice(2):''); + assert.equal(encodeFunctionData({abi,functionName:signature,args}),expectedData); + ({hash}=await writeContract({provider:nodes.validator.provider,signer,chainId:1337,nonce,address:contract, + abi,functionName:signature,args,includePublicKey:true,wait:false,gasLimit:500000, + maxFeePerGas:Number(maxFee),maxPriorityFeePerGas:0})); + } } const receipt=await waitForTransactionReceipt({provider:nodes.validator.provider,hash,timeoutMs:30000,pollIntervalMs:100}); assert.equal(receipt.status,success?'0x1':'0x0'); - assert.equal(receipt.from,signer.getAddress());assert.equal(receipt.to,contract); + assert.equal(receipt.from,signer.getAddress());assert.equal(receipt.to,entry==='AA'?null:contract); + assert.equal(receipt.type,entry==='AA'?'0x7e':'0x2'); const block=await rpc('validator','eth_getBlockByNumber',[receipt.blockNumber,false]); assert.equal(block.hash,receipt.blockHash);assert.ok(block.transactions.includes(hash)); const baseFee=BigInt(block.baseFeePerGas),price=baseFee{ const finalized=await rpc('validator','eth_getBlockByNumber',['finalized',false]); return finalized && BigInt(finalized.number)>=BigInt(receipts.at(-1).blockNumber); @@ -878,7 +923,8 @@ contract StatefulOverloads { const resumed=await start('validator');await start('follower',resumed.bootnode); for(let i=0;i{ for(const name of ['validator','follower']) { const finalized=await rpc(name,'eth_getBlockByNumber',['finalized',false]); @@ -887,7 +933,7 @@ contract StatefulOverloads { return true; },'continued write finality'); assert.deepEqual(abi,original); - console.log('canonical overload live acceptance',JSON.stringify({chainId:1337,algorithm,variant:variant.kind,ordinary:true, + console.log('canonical overload live acceptance',JSON.stringify({chainId:1337,algorithm,variant:variant.kind,entry, receipts,history,crashRecovery:true,offlineFollowerCatchup:true,continuedWrite:true,finalized:true},(_,value)=>typeof value==='bigint'?value.toString():value)); passed=true; } catch(error) { @@ -898,6 +944,7 @@ contract StatefulOverloads { if(passed)await rm(directory,{recursive:true}); } }); + } } }