Page MenuHomePhabricator

D16821.id49890.diff
No OneTemporary

D16821.id49890.diff

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

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)

Event Timeline