Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13711167
D16821.id49890.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
58 KB
Subscribers
None
D16821.id49890.diff
View Options
diff --git a/modules/ecash-agora/agora.py b/modules/ecash-agora/agora.py
--- a/modules/ecash-agora/agora.py
+++ b/modules/ecash-agora/agora.py
@@ -389,6 +389,7 @@
token_type: int
token_protocol: str
script_len: int
+ enforced_locktime: int
dust_amount: int
@classmethod
@@ -424,6 +425,7 @@
pushdata.extend(self.token_scale_factor.to_bytes(8, "little"))
pushdata.extend(self.scaled_trunc_tokens_per_trunc_sat.to_bytes(8, "little"))
pushdata.extend(self.min_accepted_scaled_trunc_tokens.to_bytes(8, "little"))
+ pushdata.extend(self.enforced_locktime.to_bytes(4, "little"))
pushdata.extend(self.maker_pk)
return bytes(pushdata)
@@ -435,6 +437,7 @@
self.token_scale_factor.to_bytes(8, "little"),
self.scaled_trunc_tokens_per_trunc_sat.to_bytes(8, "little"),
self.min_accepted_scaled_trunc_tokens.to_bytes(8, "little"),
+ self.enforced_locktime.to_bytes(4, "little"),
]
def script(self) -> CScript:
@@ -547,7 +550,11 @@
OP_NIP,
32,
OP_SPLIT,
+ 4,
+ OP_SPLIT,
OP_DROP,
+ self.enforced_locktime.to_bytes(4, "little"),
+ OP_EQUALVERIFY,
OP_TOALTSTACK,
OP_CAT,
OP_HASH160,
@@ -715,10 +722,10 @@
data_reader = BytesIO(pushdata)
# AGR0 PARTIAL pushdata always has the same length
if token.token_protocol == "SLP":
- if len(pushdata) != 59:
+ if len(pushdata) != 63:
return None
elif token.token_protocol == "ALP":
- if len(pushdata) != 71:
+ if len(pushdata) != 75:
return None
if data_reader.read(4) != b"AGR0":
return None
@@ -731,6 +738,7 @@
token_scale_factor = int.from_bytes(data_reader.read(8), "little")
scaled_trunc_tokens_per_trunc_sat = int.from_bytes(data_reader.read(8), "little")
min_accepted_scaled_trunc_tokens = int.from_bytes(data_reader.read(8), "little")
+ enforced_locktime = int.from_bytes(data_reader.read(4), "little")
maker_pk = data_reader.read(33)
token_trunc_factor = 1 << (8 * num_token_trunc_bytes)
@@ -751,6 +759,7 @@
token_type=token.token_type,
token_protocol=token.token_protocol,
script_len=0x7F,
+ enforced_locktime=enforced_locktime,
dust_amount=546,
)
measured_len = len(cut_out_codesep(partial_alp.script()))
diff --git a/modules/ecash-agora/src/agora.ts b/modules/ecash-agora/src/agora.ts
--- a/modules/ecash-agora/src/agora.ts
+++ b/modules/ecash-agora/src/agora.ts
@@ -43,6 +43,7 @@
import {
AgoraPartial,
AgoraPartialCancelSignatory,
+ AgoraPartialParams,
AgoraPartialSignatory,
} from './partial.js';
@@ -280,6 +281,7 @@
});
}
txBuild.outputs.push(...params.extraOutputs);
+ txBuild.locktime = agoraPartial.enforcedLockTime;
return txBuild;
default:
throw new Error('Not implemented');
@@ -454,6 +456,7 @@
* See agora.py.
**/
export class Agora {
+ private chronik: ChronikClient;
private plugin: PluginEndpoint;
private dustAmount: number;
@@ -462,6 +465,7 @@
* "agora" plugin loaded.
**/
public constructor(chronik: ChronikClient, dustAmount?: number) {
+ this.chronik = chronik;
this.plugin = chronik.plugin(PLUGIN_NAME);
this.dustAmount = dustAmount ?? DEFAULT_DUST_LIMIT;
}
@@ -508,6 +512,47 @@
return await this._activeOffersByGroup(PUBKEY_PREFIX + pubkeyHex);
}
+ /**
+ * Build a safe AgoraPartial for the given parameters.
+ *
+ * This looks at the blockchain to avoid creating an identical offer, by
+ * tweaking the enforcedLockTime.
+ */
+ public async selectParams(
+ params: Omit<AgoraPartialParams, 'enforcedLockTime'> | AgoraPartial,
+ ): Promise<AgoraPartial> {
+ // Assumes MTP is not more than 14 days in the past
+ const maxLockTime = new Date().getTime() / 1000 - 14 * 24 * 3600;
+ const minLockTime = 500000000;
+
+ // The probability of requiring a re-roll is only ~10^-9, but that's
+ // still high enough so we have to do it.
+ // If someone were to create 1000 identical offers, the probability of
+ // picking two conflicting locktimes would be 0.04%, and for 10000 it's
+ // even 4%, so we definitely have to check for duplicates.
+ // See https://www.bdayprob.com/, where D = 1200000000 and N = 1000
+ // (or 10000), and solve for P(D,N).
+ while (true) {
+ const enforcedLockTime =
+ Math.floor(Math.random() * (maxLockTime - minLockTime)) +
+ minLockTime;
+ const newParams =
+ params instanceof AgoraPartial
+ ? new AgoraPartial({ ...params, enforcedLockTime })
+ : AgoraPartial.approximateParams({
+ ...params,
+ enforcedLockTime,
+ });
+ const agoraScript = newParams.script();
+ const utxos = await this.chronik
+ .script('p2sh', toHex(shaRmd160(agoraScript.bytecode)))
+ .utxos();
+ if (utxos.utxos.length == 0) {
+ return newParams;
+ }
+ }
+ }
+
private async _allTokenIdsByPrefix(prefixHex: string): Promise<string[]> {
let tokenIds: string[] = [];
let nextStart: string | undefined = undefined;
@@ -625,8 +670,13 @@
tokenScaleFactorHex,
scaledTruncTokensPerTruncSatHex,
minAcceptedScaledTruncTokensHex,
+ enforcedLockTimeHex,
] = plugin.data;
+ if (enforcedLockTimeHex === undefined) {
+ throw new Error('Outdated plugin');
+ }
+
const numTokenTruncBytes = fromHex(numTokenTruncBytesHex)[0];
const numSatsTruncBytes = fromHex(numSatsTruncBytesHex)[0];
const tokenScaleFactor = new Bytes(
@@ -638,6 +688,9 @@
const minAcceptedScaledTruncTokens = new Bytes(
fromHex(minAcceptedScaledTruncTokensHex),
).readU64();
+ const enforcedLockTime = new Bytes(
+ fromHex(enforcedLockTimeHex),
+ ).readU32();
const makerPkGroupHex = plugin.groups.find(group =>
group.startsWith(PUBKEY_PREFIX),
@@ -662,6 +715,7 @@
tokenType: utxo.token.tokenType.number,
tokenProtocol: utxo.token.tokenType.protocol,
scriptLen: 0x7f,
+ enforcedLockTime,
dustAmount: this.dustAmount,
});
agoraPartial.updateScriptLen();
diff --git a/modules/ecash-agora/src/partial.approx.test.ts b/modules/ecash-agora/src/partial.approx.test.ts
--- a/modules/ecash-agora/src/partial.approx.test.ts
+++ b/modules/ecash-agora/src/partial.approx.test.ts
@@ -14,6 +14,7 @@
tokenId: '00'.repeat(32),
tokenType: SLP_FUNGIBLE,
tokenProtocol: 'SLP' as const,
+ enforcedLockTime: 1234,
dustAmount: DEFAULT_DUST_LIMIT,
};
const BASE_PARAMS_ALP = {
@@ -21,6 +22,7 @@
tokenId: '00'.repeat(32),
tokenType: ALP_STANDARD,
tokenProtocol: 'ALP' as const,
+ enforcedLockTime: 1234,
dustAmount: DEFAULT_DUST_LIMIT,
};
diff --git a/modules/ecash-agora/src/partial.script.alp.test.ts b/modules/ecash-agora/src/partial.script.alp.test.ts
--- a/modules/ecash-agora/src/partial.script.alp.test.ts
+++ b/modules/ecash-agora/src/partial.script.alp.test.ts
@@ -17,6 +17,7 @@
tokenId,
tokenProtocol: 'ALP' as const,
tokenType: ALP_STANDARD,
+ enforcedLockTime: 500000001,
dustAmount: DEFAULT_DUST_LIMIT,
};
@@ -33,19 +34,19 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(200);
+ expect(agoraPartial.scriptLen).to.equal(208);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c74534c5032000453454e448f8e8d8c8b8a898887868584838281807f7e7d7c' +
- '7b7a797877767574737271706a504741475230075041525449414c000040420f' +
- '000000000040420f000000000040420f00000000000300010203040506070809' +
- '0a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0800ca9a3b00000000ab' +
- '7b63817b6ea269760340420fa269760340420f9700887d945279012a7f757892' +
- '635357807e780340420f965667525768807e52790340420f9656807e827c7e53' +
- '79012a7f777c7e825980bc7c7e007e7b033f420f930340420f9658807e041976' +
- 'a914707501537f77a97e0288ac7e7e6b7d02220258800317a9147e024c747258' +
- '7d807e7e7e01ab7e537901257f7702c8007f5c7f7701207f756b7ea97e01877e' +
- '7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c' +
- '677501537f7768ac',
+ '4c78534c5032000453454e448f8e8d8c8b8a898887868584838281807f7e7d7c' +
+ '7b7a797877767574737271706a504b41475230075041525449414c000040420f' +
+ '000000000040420f000000000040420f00000000000165cd1d03000102030405' +
+ '060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0800ca9a3b00' +
+ '000000ab7b63817b6ea269760340420fa269760340420f9700887d945279012a' +
+ '7f757892635357807e780340420f965667525768807e52790340420f9656807e' +
+ '827c7e5379012a7f777c7e825980bc7c7e007e7b033f420f930340420f965880' +
+ '7e041976a914707501577f77a97e0288ac7e7e6b7d02220258800317a9147e02' +
+ '4c7872587d807e7e7e01ab7e537901257f7702d0007f5c7f7701207f547f7504' +
+ '0165cd1d886b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144' +
+ '807c7ea86f7bbb7501c17e7c677501577f7768ac',
);
});
@@ -61,19 +62,19 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(195);
+ expect(agoraPartial.scriptLen).to.equal(203);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c74534c5032000453454e448f8e8d8c8b8a898887868584838281807f7e7d7c' +
- '7b7a797877767574737271706a504741475230075041525449414c0101d00700' +
- '0000000000d007000000000000d0070000000000000300010203040506070809' +
- '0a0b0c0d0e0f101112131415161718191a1b1c1d1e1f08002d310100000000ab' +
- '7b63817b6ea2697602d007a2697602d0079700887d945279012a7f7578926353' +
- '58807e7802d007965667525868807e527902d0079655807e827c7e5379012a7f' +
- '777c7e825980bc7c7e01007e7b02cf079302d0079657807e041976a914707501' +
- '537f77a97e0288ac7e7e6b7d02220258800317a9147e024c7472587d807e7e7e' +
- '01ab7e537901257f7702c3007f5c7f7701207f756b7ea97e01877e7c92647500' +
- '687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501537f' +
- '7768ac',
+ '4c78534c5032000453454e448f8e8d8c8b8a898887868584838281807f7e7d7c' +
+ '7b7a797877767574737271706a504b41475230075041525449414c0101d00700' +
+ '0000000000d007000000000000d0070000000000000165cd1d03000102030405' +
+ '060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f08002d310100' +
+ '000000ab7b63817b6ea2697602d007a2697602d0079700887d945279012a7f75' +
+ '7892635358807e7802d007965667525868807e527902d0079655807e827c7e53' +
+ '79012a7f777c7e825980bc7c7e01007e7b02cf079302d0079657807e041976a9' +
+ '14707501577f77a97e0288ac7e7e6b7d02220258800317a9147e024c7872587d' +
+ '807e7e7e01ab7e537901257f7702cb007f5c7f7701207f547f75040165cd1d88' +
+ '6b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f' +
+ '7bbb7501c17e7c677501577f7768ac',
);
});
@@ -89,19 +90,19 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(209);
+ expect(agoraPartial.scriptLen).to.equal(217);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c74534c5032000453454e448f8e8d8c8b8a898887868584838281807f7e7d7c' +
- '7b7a797877767574737271706a504741475230075041525449414c0202785634' +
- '1200000000325476980000000047464500000000000300010203040506070809' +
- '0a0b0c0d0e0f101112131415161718191a1b1c1d1e1f08e8a948e6cc4f6d03ab' +
- '7b63817b6ea2697603474645a2697604785634129700887d945279012a7f7578' +
- '92635359807e780478563412965667525968807e527904785634129654807e82' +
- '7c7e5379012a7f777c7e825980bc7c7e0200007e7b0531547698009305325476' +
- '98009656807e041976a914707501537f77a97e0288ac7e7e6b7d022202588003' +
- '17a9147e024c7472587d807e7e7e01ab7e537901257f7702d1007f5c7f770120' +
- '7f756b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7e' +
- 'a86f7bbb7501c17e7c677501537f7768ac',
+ '4c78534c5032000453454e448f8e8d8c8b8a898887868584838281807f7e7d7c' +
+ '7b7a797877767574737271706a504b41475230075041525449414c0202785634' +
+ '1200000000325476980000000047464500000000000165cd1d03000102030405' +
+ '060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f08e8a948e6cc' +
+ '4f6d03ab7b63817b6ea2697603474645a2697604785634129700887d94527901' +
+ '2a7f757892635359807e780478563412965667525968807e5279047856341296' +
+ '54807e827c7e5379012a7f777c7e825980bc7c7e0200007e7b05315476980093' +
+ '0532547698009656807e041976a914707501577f77a97e0288ac7e7e6b7d0222' +
+ '0258800317a9147e024c7872587d807e7e7e01ab7e537901257f7702d9007f5c' +
+ '7f7701207f547f75040165cd1d886b7ea97e01877e7c92647500687b8292697e' +
+ '6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501577f7768ac',
);
});
@@ -117,19 +118,19 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(212);
+ expect(agoraPartial.scriptLen).to.equal(220);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c74534c5032000453454e448f8e8d8c8b8a898887868584838281807f7e7d7c' +
- '7b7a797877767574737271706a504741475230075041525449414c0303ffffff' +
- '7f00000000ffffff7f00000000ffffff7f000000000300010203040506070809' +
- '0a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0801000000ffffff3fab' +
- '7b63817b6ea2697604ffffff7fa2697604ffffff7f9700887d945279012a7f75' +
- '789263535a807e7804ffffff7f965667525a68807e527904ffffff7f96548053' +
- '7f757e827c7e5379012a7f777c7e825980bc7c7e030000007e7b04feffff7f93' +
- '04ffffff7f9655807e041976a914707501537f77a97e0288ac7e7e6b7d022202' +
- '58800317a9147e024c7472587d807e7e7e01ab7e537901257f7702d4007f5c7f' +
- '7701207f756b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144' +
- '807c7ea86f7bbb7501c17e7c677501537f7768ac',
+ '4c78534c5032000453454e448f8e8d8c8b8a898887868584838281807f7e7d7c' +
+ '7b7a797877767574737271706a504b41475230075041525449414c0303ffffff' +
+ '7f00000000ffffff7f00000000ffffff7f000000000165cd1d03000102030405' +
+ '060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0801000000ff' +
+ 'ffff3fab7b63817b6ea2697604ffffff7fa2697604ffffff7f9700887d945279' +
+ '012a7f75789263535a807e7804ffffff7f965667525a68807e527904ffffff7f' +
+ '965480537f757e827c7e5379012a7f777c7e825980bc7c7e030000007e7b04fe' +
+ 'ffff7f9304ffffff7f9655807e041976a914707501577f77a97e0288ac7e7e6b' +
+ '7d02220258800317a9147e024c7872587d807e7e7e01ab7e537901257f7702dc' +
+ '007f5c7f7701207f547f75040165cd1d886b7ea97e01877e7c92647500687b82' +
+ '92697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501577f7768ac',
);
});
@@ -145,19 +146,19 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(201);
+ expect(agoraPartial.scriptLen).to.equal(209);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c74534c5032000453454e448f8e8d8c8b8a898887868584838281807f7e7d7c' +
- '7b7a797877767574737271706a504741475230075041525449414c0304ff6f00' +
- '0000000000ff5f000000000000ff4f0000000000000300010203040506070809' +
- '0a0b0c0d0e0f101112131415161718191a1b1c1d1e1f080110ff3700000000ab' +
- '7b63817b6ea2697602ff4fa2697602ff6f9700887d945279012a7f7578926353' +
- '5a807e7802ff6f965667525a68807e527902ff6f965480537f757e827c7e5379' +
- '012a7f777c7e825980bc7c7e04000000007e7b02fe5f9302ff5f9654807e0419' +
- '76a914707501537f77a97e0288ac7e7e6b7d02220258800317a9147e024c7472' +
- '587d807e7e7e01ab7e537901257f7702c9007f5c7f7701207f756b7ea97e0187' +
- '7e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e' +
- '7c677501537f7768ac',
+ '4c78534c5032000453454e448f8e8d8c8b8a898887868584838281807f7e7d7c' +
+ '7b7a797877767574737271706a504b41475230075041525449414c0304ff6f00' +
+ '0000000000ff5f000000000000ff4f0000000000000165cd1d03000102030405' +
+ '060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f080110ff3700' +
+ '000000ab7b63817b6ea2697602ff4fa2697602ff6f9700887d945279012a7f75' +
+ '789263535a807e7802ff6f965667525a68807e527902ff6f965480537f757e82' +
+ '7c7e5379012a7f777c7e825980bc7c7e04000000007e7b02fe5f9302ff5f9654' +
+ '807e041976a914707501577f77a97e0288ac7e7e6b7d02220258800317a9147e' +
+ '024c7872587d807e7e7e01ab7e537901257f7702d1007f5c7f7701207f547f75' +
+ '040165cd1d886b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa885201' +
+ '44807c7ea86f7bbb7501c17e7c677501577f7768ac',
);
});
});
diff --git a/modules/ecash-agora/src/partial.script.slp.test.ts b/modules/ecash-agora/src/partial.script.slp.test.ts
--- a/modules/ecash-agora/src/partial.script.slp.test.ts
+++ b/modules/ecash-agora/src/partial.script.slp.test.ts
@@ -22,6 +22,7 @@
makerPk,
tokenId,
tokenProtocol: 'SLP' as const,
+ enforcedLockTime: 500000001,
dustAmount: DEFAULT_DUST_LIMIT,
};
@@ -39,19 +40,19 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(207);
+ expect(agoraPartial.scriptLen).to.equal(215);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c726a04534c500001010453454e4420707172737475767778797a7b7c7d7e7f' +
+ '4c766a04534c500001010453454e4420707172737475767778797a7b7c7d7e7f' +
'808182838485868788898a8b8c8d8e8f080000000000000000000040420f0000' +
- '00000040420f000000000040420f000000000003000102030405060708090a0b' +
- '0c0d0e0f101112131415161718191a1b1c1d1e1f0800ca9a3b00000000ab7b63' +
- '817b6ea269760340420fa269760340420f9700887d94527901377f7578926358' +
- '7e780340420f965880bc007e7e68587e52790340420f965880bc007e7e825980' +
- 'bc7c7e007e7b033f420f930340420f9658807e041976a914707501517f77a97e' +
- '0288ac7e7e6b7d02220258800317a9147e024c7272587d807e7e7e01ab7e5379' +
- '01257f7702cf007f5c7f7701207f756b7ea97e01877e7c92647500687b829269' +
- '7e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c677501517f7768ad0750' +
- '41525449414c88044147523087',
+ '00000040420f000000000040420f00000000000165cd1d030001020304050607' +
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0800ca9a3b000000' +
+ '00ab7b63817b6ea269760340420fa269760340420f9700887d94527901377f75' +
+ '789263587e780340420f965880bc007e7e68587e52790340420f965880bc007e' +
+ '7e825980bc7c7e007e7b033f420f930340420f9658807e041976a91470750155' +
+ '7f77a97e0288ac7e7e6b7d02220258800317a9147e024c7672587d807e7e7e01' +
+ 'ab7e537901257f7702d7007f5c7f7701207f547f75040165cd1d886b7ea97e01' +
+ '877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c1' +
+ '7e7c677501557f7768ad075041525449414c88044147523087',
);
});
@@ -68,19 +69,19 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(204);
+ expect(agoraPartial.scriptLen).to.equal(212);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c726a04534c500001020453454e4420707172737475767778797a7b7c7d7e7f' +
+ '4c766a04534c500001020453454e4420707172737475767778797a7b7c7d7e7f' +
'808182838485868788898a8b8c8d8e8f0800000000000000000101d007000000' +
- '000000d007000000000000d00700000000000003000102030405060708090a0b' +
- '0c0d0e0f101112131415161718191a1b1c1d1e1f08002d310100000000ab7b63' +
- '817b6ea2697602d007a2697602d0079700887d94527901377f75789263587e78' +
- '02d007965780bc01007e7e68587e527902d007965780bc01007e7e825980bc7c' +
- '7e01007e7b02cf079302d0079657807e041976a914707501517f77a97e0288ac' +
- '7e7e6b7d02220258800317a9147e024c7272587d807e7e7e01ab7e537901257f' +
- '7702cc007f5c7f7701207f756b7ea97e01877e7c92647500687b8292697e6c6c' +
- '7b7eaa88520144807c7ea86f7bbb7501c17e7c677501517f7768ad0750415254' +
- '49414c88044147523087',
+ '000000d007000000000000d0070000000000000165cd1d030001020304050607' +
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f08002d3101000000' +
+ '00ab7b63817b6ea2697602d007a2697602d0079700887d94527901377f757892' +
+ '63587e7802d007965780bc01007e7e68587e527902d007965780bc01007e7e82' +
+ '5980bc7c7e01007e7b02cf079302d0079657807e041976a914707501557f77a9' +
+ '7e0288ac7e7e6b7d02220258800317a9147e024c7672587d807e7e7e01ab7e53' +
+ '7901257f7702d4007f5c7f7701207f547f75040165cd1d886b7ea97e01877e7c' +
+ '92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c67' +
+ '7501557f7768ad075041525449414c88044147523087',
);
});
@@ -97,19 +98,20 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(220);
+ expect(agoraPartial.scriptLen).to.equal(228);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c726a04534c500001810453454e4420707172737475767778797a7b7c7d7e7f' +
+ '4c766a04534c500001810453454e4420707172737475767778797a7b7c7d7e7f' +
'808182838485868788898a8b8c8d8e8f08000000000000000002027856341200' +
- '0000003254769800000000474645000000000003000102030405060708090a0b' +
- '0c0d0e0f101112131415161718191a1b1c1d1e1f08e8a948e6cc4f6d03ab7b63' +
- '817b6ea2697603474645a2697604785634129700887d94527901377f75789263' +
- '587e780478563412965680bc0200007e7e68587e52790478563412965680bc02' +
- '00007e7e825980bc7c7e0200007e7b053154769800930532547698009656807e' +
- '041976a914707501517f77a97e0288ac7e7e6b7d02220258800317a9147e024c' +
- '7272587d807e7e7e01ab7e537901257f7702dc007f5c7f7701207f756b7ea97e' +
- '01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501' +
- 'c17e7c677501517f7768ad075041525449414c88044147523087',
+ '000000325476980000000047464500000000000165cd1d030001020304050607' +
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f08e8a948e6cc4f6d' +
+ '03ab7b63817b6ea2697603474645a2697604785634129700887d94527901377f' +
+ '75789263587e780478563412965680bc0200007e7e68587e5279047856341296' +
+ '5680bc0200007e7e825980bc7c7e0200007e7b05315476980093053254769800' +
+ '9656807e041976a914707501557f77a97e0288ac7e7e6b7d02220258800317a9' +
+ '147e024c7672587d807e7e7e01ab7e537901257f7702e4007f5c7f7701207f54' +
+ '7f75040165cd1d886b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88' +
+ '520144807c7ea86f7bbb7501c17e7c677501557f7768ad075041525449414c88' +
+ '044147523087',
);
});
@@ -126,19 +128,20 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(222);
+ expect(agoraPartial.scriptLen).to.equal(230);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c726a04534c500001010453454e4420707172737475767778797a7b7c7d7e7f' +
+ '4c766a04534c500001010453454e4420707172737475767778797a7b7c7d7e7f' +
'808182838485868788898a8b8c8d8e8f0800000000000000000303ffffff7f00' +
- '000000ffffff7f00000000ffffff7f0000000003000102030405060708090a0b' +
- '0c0d0e0f101112131415161718191a1b1c1d1e1f0801000000ffffff3fab7b63' +
- '817b6ea2697604ffffff7fa2697604ffffff7f9700887d94527901377f757892' +
- '63587e7804ffffff7f965580bc030000007e7e68587e527904ffffff7f965580' +
- 'bc030000007e7e825980bc7c7e030000007e7b04feffff7f9304ffffff7f9655' +
- '807e041976a914707501517f77a97e0288ac7e7e6b7d02220258800317a9147e' +
- '024c7272587d807e7e7e01ab7e537901257f7702de007f5c7f7701207f756b7e' +
- 'a97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb' +
- '7501c17e7c677501517f7768ad075041525449414c88044147523087',
+ '000000ffffff7f00000000ffffff7f000000000165cd1d030001020304050607' +
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0801000000ffffff' +
+ '3fab7b63817b6ea2697604ffffff7fa2697604ffffff7f9700887d9452790137' +
+ '7f75789263587e7804ffffff7f965580bc030000007e7e68587e527904ffffff' +
+ '7f965580bc030000007e7e825980bc7c7e030000007e7b04feffff7f9304ffff' +
+ 'ff7f9655807e041976a914707501557f77a97e0288ac7e7e6b7d022202588003' +
+ '17a9147e024c7672587d807e7e7e01ab7e537901257f7702e6007f5c7f770120' +
+ '7f547f75040165cd1d886b7ea97e01877e7c92647500687b8292697e6c6c7b7e' +
+ 'aa88520144807c7ea86f7bbb7501c17e7c677501557f7768ad07504152544941' +
+ '4c88044147523087',
);
});
@@ -155,19 +158,19 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(213);
+ expect(agoraPartial.scriptLen).to.equal(221);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c726a04534c500001020453454e4420707172737475767778797a7b7c7d7e7f' +
+ '4c766a04534c500001020453454e4420707172737475767778797a7b7c7d7e7f' +
'808182838485868788898a8b8c8d8e8f0800000000000000000404ff6f000000' +
- '000000ff5f000000000000ff4f00000000000003000102030405060708090a0b' +
- '0c0d0e0f101112131415161718191a1b1c1d1e1f080110ff3700000000ab7b63' +
- '817b6ea2697602ff4fa2697602ff6f9700887d94527901377f75789263587e78' +
- '02ff6f965480bc04000000007e7e68587e527902ff6f965480bc04000000007e' +
- '7e825980bc7c7e04000000007e7b02fe5f9302ff5f9654807e041976a9147075' +
- '01517f77a97e0288ac7e7e6b7d02220258800317a9147e024c7272587d807e7e' +
- '7e01ab7e537901257f7702d5007f5c7f7701207f756b7ea97e01877e7c926475' +
- '00687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c67750151' +
- '7f7768ad075041525449414c88044147523087',
+ '000000ff5f000000000000ff4f0000000000000165cd1d030001020304050607' +
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f080110ff37000000' +
+ '00ab7b63817b6ea2697602ff4fa2697602ff6f9700887d94527901377f757892' +
+ '63587e7802ff6f965480bc04000000007e7e68587e527902ff6f965480bc0400' +
+ '0000007e7e825980bc7c7e04000000007e7b02fe5f9302ff5f9654807e041976' +
+ 'a914707501557f77a97e0288ac7e7e6b7d02220258800317a9147e024c767258' +
+ '7d807e7e7e01ab7e537901257f7702dd007f5c7f7701207f547f75040165cd1d' +
+ '886b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa88520144807c7ea8' +
+ '6f7bbb7501c17e7c677501557f7768ad075041525449414c88044147523087',
);
});
@@ -184,20 +187,20 @@
scriptLen: 0x7f,
});
agoraPartial.updateScriptLen();
- expect(agoraPartial.scriptLen).to.equal(233);
+ expect(agoraPartial.scriptLen).to.equal(241);
expect(toHex(agoraPartial.script().bytecode)).to.equal(
- '4c726a04534c500001010453454e4420707172737475767778797a7b7c7d7e7f' +
+ '4c766a04534c500001010453454e4420707172737475767778797a7b7c7d7e7f' +
'808182838485868788898a8b8c8d8e8f0800000000000000000504ff3f120800' +
- '000000ff3f120400000000ff3f12020000000003000102030405060708090a0b' +
- '0c0d0e0f101112131415161718191a1b1c1d1e1f0801c0edf63f120800ab7b63' +
- '817b6ea2697604ff3f1202a2697604ff3f12089700887d94527901377f757892' +
- '63587e7804ff3f1208965480537f75bc0500000000007e7e68587e527904ff3f' +
- '1208965480537f75bc0500000000007e7e825980bc7c7e04000000007e7b04fe' +
- '3f12049304ff3f12049654807e041976a914707501517f77a97e0288ac7e7e6b' +
- '7d02220258800317a9147e024c7272587d807e7e7e01ab7e537901257f7702e9' +
- '007f5c7f7701207f756b7ea97e01877e7c92647500687b8292697e6c6c7b7eaa' +
- '88520144807c7ea86f7bbb7501c17e7c677501517f7768ad075041525449414c' +
- '88044147523087',
+ '000000ff3f120400000000ff3f1202000000000165cd1d030001020304050607' +
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0801c0edf63f1208' +
+ '00ab7b63817b6ea2697604ff3f1202a2697604ff3f12089700887d9452790137' +
+ '7f75789263587e7804ff3f1208965480537f75bc0500000000007e7e68587e52' +
+ '7904ff3f1208965480537f75bc0500000000007e7e825980bc7c7e0400000000' +
+ '7e7b04fe3f12049304ff3f12049654807e041976a914707501557f77a97e0288' +
+ 'ac7e7e6b7d02220258800317a9147e024c7672587d807e7e7e01ab7e53790125' +
+ '7f7702f1007f5c7f7701207f547f75040165cd1d886b7ea97e01877e7c926475' +
+ '00687b8292697e6c6c7b7eaa88520144807c7ea86f7bbb7501c17e7c67750155' +
+ '7f7768ad075041525449414c88044147523087',
);
});
});
diff --git a/modules/ecash-agora/src/partial.ts b/modules/ecash-agora/src/partial.ts
--- a/modules/ecash-agora/src/partial.ts
+++ b/modules/ecash-agora/src/partial.ts
@@ -119,6 +119,18 @@
tokenType: number;
/** Token protocol of the offered token. */
tokenProtocol: 'SLP' | 'ALP';
+ /**
+ * Locktime enforced by the Script. Used to make identical offers unique.
+ * If there's two offers with identical terms, it would be possible to burn
+ * one of them by accepting both in one transaction.
+ * To prevent this for identical offers, set to unique (past) locktimes.
+ *
+ * The simplest way would be to just pick a random locktime between
+ * 500000000 and now (in UNIX timestamp).
+ * When creating offers in a loop, it's better to increment this each offer
+ * instead of drawing a new random one each offer.
+ **/
+ enforcedLockTime: number;
/** Dust amount to be used by the script. */
dustAmount?: number;
/**
@@ -242,6 +254,18 @@
public tokenProtocol: 'SLP' | 'ALP';
/** Byte length of the Script, after OP_CODESEPARATOR. */
public scriptLen: number;
+ /**
+ * Locktime enforced by the Script. Used to make identical offers unique.
+ * If there's two offers with identical terms, it would be possible to burn
+ * one of them by accepting both in one transaction.
+ * To prevent this for identical offers, set to unique (past) locktimes.
+ *
+ * The simplest way would be to just pick a random locktime between
+ * 500000000 and now (in UNIX timestamp).
+ * When creating offers in a loop, it's better to increment this each offer
+ * instead of drawing a new random one each offer.
+ **/
+ public enforcedLockTime: number;
/**
* Dust amount of the network, the Script will enforce token outputs to have
* this amount.
@@ -260,6 +284,7 @@
tokenType: number;
tokenProtocol: 'SLP' | 'ALP';
scriptLen: number;
+ enforcedLockTime: number;
dustAmount: number;
}) {
this.truncTokens = params.truncTokens;
@@ -273,6 +298,7 @@
this.tokenType = params.tokenType;
this.tokenProtocol = params.tokenProtocol;
this.scriptLen = params.scriptLen;
+ this.enforcedLockTime = params.enforcedLockTime;
this.dustAmount = params.dustAmount;
}
@@ -432,6 +458,7 @@
tokenType: params.tokenType,
tokenProtocol: params.tokenProtocol,
scriptLen: 0x7f,
+ enforcedLockTime: params.enforcedLockTime,
dustAmount: params.dustAmount ?? DEFAULT_DUST_LIMIT,
});
if (agoraPartial.minAcceptedTokens() < 1n) {
@@ -536,6 +563,7 @@
writer.putU64(this.tokenScaleFactor);
writer.putU64(this.scaledTruncTokensPerTruncSat);
writer.putU64(this.minAcceptedScaledTruncTokens);
+ writer.putU32(this.enforcedLockTime);
writer.putBytes(this.makerPk);
};
const lengthWriter = new WriterLength();
@@ -592,6 +620,10 @@
);
const scaledTruncTokens8Le = scaledTruncTokens8LeWriter.data;
+ const enforcedLockTime4LeWriter = new WriterBytes(4);
+ enforcedLockTime4LeWriter.putU32(this.enforcedLockTime);
+ const enforcedLockTime4Le = enforcedLockTime4LeWriter.data;
+
return Script.fromOps([
// # Push consts
pushBytesOp(covenantConsts),
@@ -813,8 +845,14 @@
pushNumberOp(32),
// actualHashOutputs, preimage9_10 = OP_SPLIT(preimage8_10, 32)
OP_SPLIT,
- // OP_DROP(preimage9_10)
+ pushNumberOp(4),
+ // actualLocktime4Le, sighashType = OP_SPLIT(preimage9_10)
+ OP_SPLIT,
+ // OP_DROP(preimage10)
OP_DROP,
+ pushBytesOp(enforcedLockTime4Le),
+ // OP_EQUALVERIFY(actualLocktime4Le, enforcedLockTime4Le)
+ OP_EQUALVERIFY,
// # Move to altstack, will be needed later
// OP_TOALTSTACK(actualHashOutputs)
OP_TOALTSTACK,
diff --git a/modules/ecash-agora/tests/partial-helper-alp.ts b/modules/ecash-agora/tests/partial-helper-alp.ts
--- a/modules/ecash-agora/tests/partial-helper-alp.ts
+++ b/modules/ecash-agora/tests/partial-helper-alp.ts
@@ -7,6 +7,7 @@
ALL_BIP143,
alpGenesis,
alpSend,
+ Amount,
Ecc,
emppScript,
P2PKHSignatory,
@@ -14,25 +15,21 @@
shaRmd160,
TxBuilder,
TxBuilderInput,
+ TxBuilderOutput,
} from 'ecash-lib';
import { expect } from 'chai';
import { AgoraPartial } from '../src/partial.js';
import { Agora, AgoraOffer } from '../src/agora.js';
-export async function makeAlpOffer(params: {
- chronik: ChronikClient;
+export function makeAlpGenesis(params: {
ecc: Ecc;
- agoraPartial: AgoraPartial;
- makerSk: Uint8Array;
+ tokenType: number;
fuelInput: TxBuilderInput;
-}): Promise<AgoraOffer> {
- const { chronik, ecc, agoraPartial, makerSk, fuelInput } = params;
- const makerPk = ecc.derivePubkey(makerSk);
- const makerPkh = shaRmd160(makerPk);
- const makerP2pkh = Script.p2pkh(makerPkh);
-
- const genesisOutputSats = 2000;
+ tokenAmounts: Amount[];
+ extraOutputs: TxBuilderOutput[];
+}) {
+ const { ecc, tokenType, fuelInput } = params;
const txBuildGenesisGroup = new TxBuilder({
inputs: [fuelInput],
outputs: [
@@ -40,22 +37,44 @@
value: 0,
script: emppScript([
alpGenesis(
- agoraPartial.tokenType,
+ tokenType,
{
- tokenTicker: `ALP token type ${agoraPartial.tokenType}`,
+ tokenTicker: `ALP token type ${tokenType}`,
decimals: 4,
},
{
numBatons: 0,
- amounts: [agoraPartial.offeredTokens()],
+ amounts: params.tokenAmounts,
},
),
]),
},
- { value: genesisOutputSats, script: makerP2pkh },
+ ...params.extraOutputs,
],
});
- const genesisTx = txBuildGenesisGroup.sign(ecc);
+ return txBuildGenesisGroup.sign(ecc);
+}
+
+export async function makeAlpOffer(params: {
+ chronik: ChronikClient;
+ ecc: Ecc;
+ agoraPartial: AgoraPartial;
+ makerSk: Uint8Array;
+ fuelInput: TxBuilderInput;
+}): Promise<AgoraOffer> {
+ const { chronik, ecc, agoraPartial, makerSk, fuelInput } = params;
+ const makerPk = ecc.derivePubkey(makerSk);
+ const makerPkh = shaRmd160(makerPk);
+ const makerP2pkh = Script.p2pkh(makerPkh);
+
+ const genesisOutputSats = 2000;
+ const genesisTx = makeAlpGenesis({
+ ecc,
+ tokenType: agoraPartial.tokenType,
+ fuelInput,
+ tokenAmounts: [agoraPartial.offeredTokens()],
+ extraOutputs: [{ value: genesisOutputSats, script: makerP2pkh }],
+ });
const genesisTxid = (await chronik.broadcastTx(genesisTx.ser())).txid;
const tokenId = genesisTxid;
agoraPartial.tokenId = tokenId;
diff --git a/modules/ecash-agora/tests/partial.alp.bigsats.test.ts b/modules/ecash-agora/tests/partial.alp.bigsats.test.ts
--- a/modules/ecash-agora/tests/partial.alp.bigsats.test.ts
+++ b/modules/ecash-agora/tests/partial.alp.bigsats.test.ts
@@ -24,6 +24,7 @@
import { AgoraPartial } from '../src/partial.js';
import { makeAlpOffer, takeAlpOffer } from './partial-helper-alp.js';
+import { Agora } from '../src/agora.js';
use(chaiAsPromised);
@@ -104,7 +105,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 0xffffffffffffn,
priceNanoSatsPerToken: 2600000n, // scaled to use the XEC
makerPk: makerPk,
@@ -122,7 +124,8 @@
makerPk,
minAcceptedScaledTruncTokens: 32511n,
...BASE_PARAMS_ALP,
- scriptLen: 196,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 204,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(0xffffff000000n);
@@ -204,7 +207,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 0xffffffffffffn,
priceNanoSatsPerToken: 30000000000000n, // scaled to use the XEC
makerPk,
@@ -221,7 +225,8 @@
makerPk,
minAcceptedScaledTruncTokens: 128n,
...BASE_PARAMS_ALP,
- scriptLen: 197,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 205,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(0xffffff000000n);
@@ -314,7 +319,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 0x7fffffffffffn,
priceNanoSatsPerToken: 5000000n, // scaled to use the XEC
makerPk: makerPk,
@@ -331,7 +337,8 @@
makerPk,
minAcceptedScaledTruncTokens: 0xffffn,
...BASE_PARAMS_ALP,
- scriptLen: 191,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 199,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(0x7fffff380000n);
@@ -414,7 +421,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 0x7fffffffffffn,
priceNanoSatsPerToken: 32000000000000n,
makerPk,
@@ -431,7 +439,8 @@
makerPk,
minAcceptedScaledTruncTokens: 256n,
...BASE_PARAMS_ALP,
- scriptLen: 197,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 205,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(0x7fffff000000n);
@@ -524,7 +533,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 100n,
priceNanoSatsPerToken: 7123456780n * 1000000000n, // scaled to use the XEC
makerPk: makerPk,
@@ -541,7 +551,8 @@
makerPk,
minAcceptedScaledTruncTokens: 0x7fff3a28n / 100n,
...BASE_PARAMS_ALP,
- scriptLen: 207,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 215,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(100n);
@@ -620,7 +631,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 100n,
priceNanoSatsPerToken: 712345678000n * 1000000000n, // scaled to use the XEC
makerPk: makerPk,
@@ -637,7 +649,8 @@
makerPk,
minAcceptedScaledTruncTokens: 0x7ffe05f4n / 100n,
...BASE_PARAMS_ALP,
- scriptLen: 208,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 216,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(100n);
diff --git a/modules/ecash-agora/tests/partial.alp.test.ts b/modules/ecash-agora/tests/partial.alp.test.ts
--- a/modules/ecash-agora/tests/partial.alp.test.ts
+++ b/modules/ecash-agora/tests/partial.alp.test.ts
@@ -438,7 +438,8 @@
for (const testCase of TEST_CASES) {
it(`AgoraPartial ALP ${testCase.offeredTokens} for ${testCase.info}`, async () => {
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: testCase.offeredTokens,
priceNanoSatsPerToken: testCase.priceNanoSatsPerToken,
minAcceptedTokens: testCase.acceptedTokens,
@@ -469,7 +470,6 @@
});
const acceptTx = await chronik.tx(acceptTxid);
const offeredTokens = agoraPartial.offeredTokens();
- const agora = new Agora(chronik);
if (testCase.acceptedTokens == offeredTokens) {
// FULL ACCEPT
// 0th output is OP_RETURN eMPP AGR0 ad + ALP SEND
diff --git a/modules/ecash-agora/tests/partial.locktime.test.ts b/modules/ecash-agora/tests/partial.locktime.test.ts
new file mode 100644
--- /dev/null
+++ b/modules/ecash-agora/tests/partial.locktime.test.ts
@@ -0,0 +1,250 @@
+// Copyright (c) 2024 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import { assert, expect, use } from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import { ChronikClient } from 'chronik-client';
+import {
+ ALL_BIP143,
+ ALP_STANDARD,
+ DEFAULT_DUST_LIMIT,
+ Ecc,
+ P2PKHSignatory,
+ SLP_FUNGIBLE,
+ Script,
+ TxBuilder,
+ TxBuilderInput,
+ alpSend,
+ emppScript,
+ fromHex,
+ initWasm,
+ shaRmd160,
+ slpSend,
+ toHex,
+} from 'ecash-lib';
+import { TestRunner } from 'ecash-lib/dist/test/testRunner.js';
+
+import { AgoraPartial, AgoraPartialSignatory } from '../src/partial.js';
+import { makeSlpOffer, takeSlpOffer } from './partial-helper-slp.js';
+import { Agora, AgoraOffer } from '../src/agora.js';
+import { makeAlpGenesis } from './partial-helper-alp.js';
+
+use(chaiAsPromised);
+
+// This test needs a lot of sats
+const NUM_COINS = 500;
+const COIN_VALUE = 1000000;
+
+const BASE_PARAMS_ALP = {
+ tokenId: '00'.repeat(32), // filled in later
+ tokenType: ALP_STANDARD,
+ tokenProtocol: 'ALP' as const,
+ dustAmount: DEFAULT_DUST_LIMIT,
+};
+
+describe('AgoraPartial enforcedLockTime', () => {
+ let runner: TestRunner;
+ let chronik: ChronikClient;
+ let ecc: Ecc;
+
+ let makerSk: Uint8Array;
+ let makerPk: Uint8Array;
+ let makerPkh: Uint8Array;
+ let makerScript: Script;
+ let makerScriptHex: string;
+ let takerSk: Uint8Array;
+ let takerPk: Uint8Array;
+ let takerPkh: Uint8Array;
+ let takerScript: Script;
+ let takerScriptHex: string;
+
+ async function makeBuilderInputs(
+ values: number[],
+ ): Promise<TxBuilderInput[]> {
+ const txid = await runner.sendToScript(values, makerScript);
+ return values.map((value, outIdx) => ({
+ input: {
+ prevOut: {
+ txid,
+ outIdx,
+ },
+ signData: {
+ value,
+ outputScript: makerScript,
+ },
+ },
+ signatory: P2PKHSignatory(makerSk, makerPk, ALL_BIP143),
+ }));
+ }
+
+ before(async () => {
+ await initWasm();
+ runner = await TestRunner.setup('setup_scripts/ecash-agora_base');
+ chronik = runner.chronik;
+ ecc = runner.ecc;
+ await runner.setupCoins(NUM_COINS, COIN_VALUE);
+
+ makerSk = fromHex('33'.repeat(32));
+ makerPk = ecc.derivePubkey(makerSk);
+ makerPkh = shaRmd160(makerPk);
+ makerScript = Script.p2pkh(makerPkh);
+ makerScriptHex = toHex(makerScript.bytecode);
+ takerSk = fromHex('44'.repeat(32));
+ takerPk = ecc.derivePubkey(takerSk);
+ takerPkh = shaRmd160(takerPk);
+ takerScript = Script.p2pkh(takerPkh);
+ takerScriptHex = toHex(takerScript.bytecode);
+ });
+
+ after(() => {
+ runner.stop();
+ });
+
+ it('AgoraPartial enforcedLockTime', async () => {
+ const LOCKTIME1 = 500000123;
+ const LOCKTIME2 = 500000999;
+ const agoraPartial = AgoraPartial.approximateParams({
+ offeredTokens: 1000n,
+ priceNanoSatsPerToken: 1000000000000n,
+ minAcceptedTokens: 1n,
+ makerPk,
+ ...BASE_PARAMS_ALP,
+ enforcedLockTime: LOCKTIME1,
+ });
+ const askedSats = agoraPartial.askedSats(100n);
+ const requiredSats = askedSats + 2000n;
+ const [fuelInput, takerInput] = await makeBuilderInputs([
+ 8000,
+ Number(requiredSats),
+ ]);
+
+ const genesisOutputSats = 2000;
+ const genesisTx = makeAlpGenesis({
+ ecc,
+ tokenType: agoraPartial.tokenType,
+ fuelInput,
+ tokenAmounts: [
+ agoraPartial.offeredTokens(),
+ agoraPartial.offeredTokens(),
+ agoraPartial.offeredTokens(),
+ ],
+ extraOutputs: [
+ { value: genesisOutputSats, script: makerScript },
+ { value: genesisOutputSats, script: makerScript },
+ { value: genesisOutputSats, script: makerScript },
+ ],
+ });
+ const genesisTxid = (await chronik.broadcastTx(genesisTx.ser())).txid;
+ const tokenId = genesisTxid;
+ agoraPartial.tokenId = tokenId;
+
+ for (let offerIdx = 0; offerIdx < 3; ++offerIdx) {
+ const offerPartial = new AgoraPartial(agoraPartial);
+ if (offerIdx == 2) {
+ offerPartial.enforcedLockTime = LOCKTIME2;
+ }
+ const agoraScript = offerPartial.script();
+ const agoraP2sh = Script.p2sh(shaRmd160(agoraScript.bytecode));
+ const txBuildOffer = new TxBuilder({
+ inputs: [
+ {
+ input: {
+ prevOut: {
+ txid: genesisTxid,
+ outIdx: offerIdx + 1,
+ },
+ signData: {
+ value: genesisOutputSats,
+ outputScript: makerScript,
+ },
+ },
+ signatory: P2PKHSignatory(makerSk, makerPk, ALL_BIP143),
+ },
+ ],
+ outputs: [
+ {
+ value: 0,
+ script: emppScript([
+ offerPartial.adPushdata(),
+ alpSend(tokenId, offerPartial.tokenType, [
+ offerPartial.offeredTokens(),
+ ]),
+ ]),
+ },
+ { value: 546, script: agoraP2sh },
+ ],
+ });
+ const offerTx = txBuildOffer.sign(ecc);
+ await chronik.broadcastTx(offerTx.ser());
+ }
+
+ const agora = new Agora(chronik);
+ const offers: (AgoraOffer & { variant: { type: 'PARTIAL' } })[] =
+ (await agora.activeOffersByTokenId(tokenId)) as any;
+ expect(offers.length).to.equal(3);
+
+ const offersLocktime1 = offers.filter(
+ offer => offer.variant.params.enforcedLockTime == LOCKTIME1,
+ );
+ const offersLocktime2 = offers.filter(
+ offer => offer.variant.params.enforcedLockTime == LOCKTIME2,
+ );
+
+ // Cannot co-spend other identical offer (due to different locktime)
+ const failedAcceptTx = offersLocktime1[0].acceptTx({
+ ecc,
+ covenantSk: takerSk,
+ covenantPk: takerPk,
+ fuelInputs: [
+ takerInput,
+ {
+ input: offersLocktime2[0].txBuilderInput,
+ signatory: AgoraPartialSignatory(
+ offersLocktime2[0].variant.params,
+ 1n,
+ takerSk,
+ takerPk,
+ ),
+ },
+ ],
+ recipientScript: takerScript,
+ acceptedTokens: 1n,
+ });
+ await assert.isRejected(
+ chronik.broadcastTx(failedAcceptTx.ser(), true),
+ 'Failed getting /broadcast-tx: 400: Broadcast failed: Transaction rejected by mempool: mandatory-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation)',
+ );
+
+ // This example shows that it's important to choose different locktimes
+ // for identical other offers, otherwise someone can spend them in a
+ // single transaction, burning one of the offers.
+ const successAcceptTx = offersLocktime1[0].acceptTx({
+ ecc,
+ covenantSk: takerSk,
+ covenantPk: takerPk,
+ fuelInputs: [
+ takerInput,
+ {
+ input: offersLocktime1[1].txBuilderInput,
+ signatory: AgoraPartialSignatory(
+ offersLocktime1[1].variant.params,
+ 1n,
+ takerSk,
+ takerPk,
+ ),
+ },
+ ],
+ recipientScript: takerScript,
+ acceptedTokens: 1n,
+ });
+ const acceptTxid = (
+ await chronik.broadcastTx(successAcceptTx.ser(), true)
+ ).txid;
+
+ const acceptTx = await chronik.tx(acceptTxid);
+ expect(acceptTx.tokenEntries[0].burnSummary).to.equal(
+ 'Unexpected burn: Burns 1000 base tokens',
+ );
+ });
+});
diff --git a/modules/ecash-agora/tests/partial.slp.bigsats.test.ts b/modules/ecash-agora/tests/partial.slp.bigsats.test.ts
--- a/modules/ecash-agora/tests/partial.slp.bigsats.test.ts
+++ b/modules/ecash-agora/tests/partial.slp.bigsats.test.ts
@@ -25,6 +25,7 @@
import { AgoraPartial } from '../src/partial.js';
import { makeSlpOffer, takeSlpOffer } from './partial-helper-slp.js';
+import { Agora } from '../src/agora.js';
use(chaiAsPromised);
@@ -105,7 +106,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 0xffffffffffffffffn,
priceNanoSatsPerToken: 40n, // scaled to use the XEC
makerPk: makerPk,
@@ -123,7 +125,8 @@
makerPk,
minAcceptedScaledTruncTokens: 32511n,
...BASE_PARAMS_SLP,
- scriptLen: 216,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 224,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(0xffffff0000000000n);
@@ -204,7 +207,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 0xffffffffffffffffn,
priceNanoSatsPerToken: 500000000n, // scaled to use the XEC
makerPk,
@@ -221,7 +225,8 @@
makerPk,
minAcceptedScaledTruncTokens: 0x7fn,
...BASE_PARAMS_SLP,
- scriptLen: 216,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 224,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(0xffffff0000000000n);
@@ -311,7 +316,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 0x7fffffffffffffffn,
priceNanoSatsPerToken: 80n, // scaled to use the XEC
makerPk: makerPk,
@@ -329,7 +335,8 @@
makerPk,
minAcceptedScaledTruncTokens: 0xffffn,
...BASE_PARAMS_SLP,
- scriptLen: 206,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 214,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(0x7fffff4200000000n);
@@ -412,7 +419,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 0x7fffffffffffffffn,
priceNanoSatsPerToken: 1000000000n,
makerPk,
@@ -429,7 +437,8 @@
makerPk,
minAcceptedScaledTruncTokens: 1n,
...BASE_PARAMS_SLP,
- scriptLen: 201,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 209,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(0x7fffffff00000000n);
@@ -519,7 +528,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 100n,
priceNanoSatsPerToken: 7123456780n * 1000000000n, // scaled to use the XEC
makerPk: makerPk,
@@ -536,7 +546,8 @@
makerPk,
minAcceptedScaledTruncTokens: 0x7fff3a28n / 100n,
...BASE_PARAMS_SLP,
- scriptLen: 214,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 222,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(100n);
@@ -610,7 +621,8 @@
BIGSATS,
]);
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: 100n,
priceNanoSatsPerToken: 712345678000n * 1000000000n, // scaled to use the XEC
makerPk: makerPk,
@@ -627,7 +639,8 @@
makerPk,
minAcceptedScaledTruncTokens: 0x7ffe05f4n / 100n,
...BASE_PARAMS_SLP,
- scriptLen: 215,
+ enforcedLockTime: agoraPartial.enforcedLockTime,
+ scriptLen: 223,
}),
);
expect(agoraPartial.offeredTokens()).to.equal(100n);
diff --git a/modules/ecash-agora/tests/partial.slp.test.ts b/modules/ecash-agora/tests/partial.slp.test.ts
--- a/modules/ecash-agora/tests/partial.slp.test.ts
+++ b/modules/ecash-agora/tests/partial.slp.test.ts
@@ -493,7 +493,8 @@
for (const testCase of TEST_CASES) {
it(`AgoraPartial SLP ${testCase.offeredTokens} for ${testCase.info}`, async () => {
- const agoraPartial = AgoraPartial.approximateParams({
+ const agora = new Agora(chronik);
+ const agoraPartial = await agora.selectParams({
offeredTokens: testCase.offeredTokens,
priceNanoSatsPerToken: testCase.priceNanoSatsPerToken,
minAcceptedTokens: testCase.acceptedTokens,
@@ -524,7 +525,6 @@
});
const acceptTx = await chronik.tx(acceptTxid);
const offeredTokens = agoraPartial.offeredTokens();
- const agora = new Agora(chronik);
if (testCase.acceptedTokens == offeredTokens) {
// FULL ACCEPT
// 0th output is OP_RETURN SLP SEND
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 26, 10:38 (5 h, 14 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573311
Default Alt Text
D16821.id49890.diff (58 KB)
Attached To
D16821: [ecash-agora] Fix issue with AgoraPartial scripts, allowing for burned tokens
Event Timeline
Log In to Comment